12  Mineral deposits

In the mining industry, resource estimation consists of interpolating measurements of metal and mineral grades from drill hole samples to 3D grids known as “block models”. Due to highly skewed distributions, several pre-processing steps need to be performed before the actual interpolation. In this chapter, we will cover simple steps for resource estimation and economic assessment of a real mineral deposit.

TOOLS COVERED: @groupby, @transform, @combine, CLR, ProjectionPursuit, EmpiricalVariogram, Kriging, Interpolate, InterpolateNeighbors, Shadow, Map, Filter, boundingbox, convexhull, viewer

MODULES:

# framework
using GeoStats

# IO modules
using GeoIO

# viz modules
using PairPlots
import CairoMakie as Mke
Note

Although we use CairoMakie.jl in this book, many of the 3D visualizations in this chapter demand a more performant Makie.jl backend. Consider using GLMakie.jl if you plan to reproduce the code locally.

12.1 Data

The GeoMet dataset (Hoffimann et al. 2022a) consists of three geospatial tables stored as CSV files. In this chapter, we will only use the drillholes.csv table.

Drill hole samples are always available in mining projects. They contain chemical information for each rock sample (a cylinder) along the drill hole trajectories. In this case, the data has been processed, and only the Cartesian “X”, “Y”, “Z” coordinates of the centroids of the cylinders were stored:

url = "https://zenodo.org/record/7051975/files/drillholes.csv?download=1"

csv = download(url, tempname()*".csv")

dtable = GeoIO.load(csv, coords = ("X", "Y", "Z"))

dtable |> Select("Cu ppm") |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227
dtable |> describe
Table with 6 columns and 19 rows:
      variable  mean      minimum  median    maximum   nmissing
    ┌──────────────────────────────────────────────────────────
 1  │ HOLEID    nothing   1        nothing   119       0
 2  │ Ag ppm    1.86199   0.01     1.14      15.38     0
 3  │ Al ppm    52374.8   2400.0   56100.0   103300.0  0
 4  │ Au ppm    0.470965  0.0      0.21      5.89      0
 5  │ C ppm     1117.7    100.0    700.0     20700.0   0
 6  │ Ca ppm    9900.6    100.0    6400.0    73700.0   0
 7  │ Cl ppm    2303.75   5.48     1756.23   14273.8   0
 8  │ Cu ppm    7540.4    0.0      5000.0    60200.0   0
 9  │ F ppm     2528.58   20.89    1610.47   19786.3   0
 10 │ Fe ppm    245452.0  9900.0   246000.0  500000.0  0
 11 │ K ppm     13610.9   100.0    12450.0   45500.0   0
 12 │ Mg ppm    17673.0   200.0    14000.0   88800.0   0
 13 │ Mn ppm    4282.96   88.95    3685.35   24812.8   0
 14 │ Na ppm    4404.8    100.0    1400.0    58900.0   0
 15 │ P ppm     720.896   38.08    630.705   5576.14   0
 16 │ Pb ppm    9.3938    0.8      6.005     247.83    0
 17 │ S ppm     2712.95   100.0    1700.0    21500.0   0
 18 │ Th ppm    8.37926   0.37     4.08      94.81     0
 19 │ U ppm     19.0894   0.21     10.825    521.49    0

There are 18 chemical elements in the table, all measured in parts per million (ppm). The table also stores an integer identifier for each hole trajectory in the “HOLEID” column. There are 119 such trajectories as shown in the “maximum” column of the describe output.

Note

In most mining projects, the drill hole samples are available as “SURVEY”, “COLLAR” and “INTERVAL” tables, which can be desurveyed and composited with DrillHoles.jl.

12.2 Objectives

Our main objective is to estimate the economic value associated with each mining block in a 3D block model, i.e. a CartesianGrid with Hexahedron geometries (the blocks). This economic value in U$ dollars is estimated in terms of various other geospatial variables:

\[ Value = \underbrace{V \times \rho \times Cu \times f \times P}_{\text{revenue}} - \underbrace{V \times \rho \times (C_m + C_p)}_{\text{cost}} \]

where

  • \(V\) is the volume of the block in \(m^3\)
  • \(\rho\) is the rock density in \(ton/m^3\)
  • \(Cu\) is the grade of copper in \([0,1]\)
  • \(f\) is the recovery of copper in \([0,1]\)
  • \(P\) is the selling price in \(U\$/ton\)
  • \(C_m\) is the mining cost in \(U\$/ton\)
  • \(C_p\) is the plant cost in \(U\$/ton\)

Secondary objectives include the localization (through 3D visualization) of blocks with high economic value, high grades of Au and Ag, and low grade of S.

For simplicity, we assume the following constants:

  • \(\rho = 2.75\ ton / m^3\)
  • \(P = 4000\ U\$ / ton\)
  • \(C_m = 4\ U\$ / ton\)
  • \(C_p = 10\ U\$ / ton\)

12.3 Methodology

In order to estimate the economic value of each mining block, we need to interpolate the grade of Cu. Because we also want to localize the blocks with high grades of Au and Ag, and low grade of S, we will perform multivariate geostatistical interpolation of Cu, Au, Ag and S.

The proposed methodology has the following steps:

  1. Preliminary analysis and processing
  2. Definition of interpolation domain
  3. Multivariate geostatistical interpolation
  4. Economic assessment and visualizations

12.3.1 Preliminary analysis

We recommend to start any application discarding all information that is not relevant for the stated objectives. In this case, the geotable contains measurements of various chemical elements that are not used in the economic assessment. We define a cleaning pipeline that selects and renames columns of interest, and adds units to the measurements:

selectholeid = Select("HOLEID")

selectgrades = Select("Cu ppm" => "Cu",
                      "Au ppm" => "Au",
                      "Ag ppm" => "Ag",
                      "S ppm"  => "S") 
               Functional(x -> 1e-4*x*u"percent") # 1 ppm = 1e-4 percent

dclean = selectholeid  selectgrades
ParallelTableTransform
├─ Select(selector: [:HOLEID], newnames: nothing)
└─ SequentialTransform
   ├─ Select(selector: [Symbol("Cu ppm"), Symbol("Au ppm"), Symbol("Ag ppm"), Symbol("S ppm")], newnames: [:Cu, :Au, :Ag, :S])
   └─ Functional(selector: all, fun: #11)
dtable = dclean(dtable)
2000×6 GeoTable over 2000 PointSet
HOLEID Cu Au Ag S geometry
Categorical Continuous Continuous Continuous Continuous Point
[NoUnits] [%] [%] [%] [%] 🖈 Cartesian{NoDatum}
1 0.72 % 0.000104 % 6.4e-5 % 0.61 % (x: 559.725 m, y: -513.31 m, z: 252.82 m)
1 0.1 % 6.0e-6 % 2.0e-5 % 0.06 % (x: 558.955 m, y: -515.23 m, z: 246.87 m)
1 2.18 % 0.000202 % 0.000344 % 1.17 % (x: 557.225 m, y: -519.61 m, z: 233.37 m)
1 2.29 % 0.000343 % 0.000488 % 1.1 % (x: 555.375 m, y: -524.3 m, z: 218.92 m)
1 0.59 % 5.0e-5 % 0.000125 % 0.22 % (x: 553.825 m, y: -528.21 m, z: 206.94 m)
1 0.56 % 9.0e-5 % 9.0e-5 % 0.28 % (x: 552.075 m, y: -532.56 m, z: 193.91 m)
1 2.0 % 0.000356 % 9.3e-5 % 1.96 % (x: 550.305 m, y: -537.08 m, z: 180.8 m)
1 0.28 % 2.1e-5 % 7.6e-5 % 0.14 % (x: 547.495 m, y: -544.31 m, z: 160.08 m)
1 0.61 % 2.0e-5 % 0.000112 % 0.28 % (x: 544.335 m, y: -552.43 m, z: 137.08 m)
1 0.25 % 1.6e-5 % 7.7e-5 % 0.09 % (x: 541.695 m, y: -559.19 m, z: 117.96 m)

In order to better understand the multivariate distribution of chemical elements, we visualize the values of the drill hole samples with the pairplot:

dtable |> Select("Cu", "Au", "Ag", "S") |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

We can observe that the distribution is very skewed.

12.3.2 Domain of interpolation

Before we can interpolate these variables, we need to define our domain of interpolation. In this application, we will define a 3D CartesianGrid in terms of the drill hole trajectories alone. Some of the Hexahedron geometries will be disabled whenever they are outside the convexhull of the points.

First, let’s create our full CartesianGrid using the boundingbox of the trajectories:

# compute bounding box
bbox = boundingbox(dtable.geometry)

# size of blocks in meters
bsize = (25.0u"m", 25.0u"m", 12.5u"m")

# define Cartesian grid
grid = CartesianGrid(extrema(bbox)..., bsize)
85×66×66 CartesianGrid
├─ minimum: Point(x: -1150.004825000069 m, y: -882.9003199972212 m, z: -399.92 m)
├─ maximum: Point(x: 974.9951749999309 m, y: 767.0996800027788 m, z: 425.08 m)
└─ spacing: (25.0 m, 25.0 m, 12.5 m)
viz(dtable.geometry, color = "black")
viz!(grid, alpha = 0.2)
Mke.current_figure()
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

Second, let’s compute the convexhull of the Shadow of all points on the xy plane:

shadow(point) = point |> Shadow("xy")

points = shadow.(dtable.geometry)

chull = convexhull(points)
PolyArea
  outer
  └─ Ring((x: -1150.0 m, y: 641.23 m), ..., (x: -1120.56 m, y: 701.22 m))
viz(chull)
viz!(points, color = "black")
Mke.current_figure()
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

We can filter the grid to retain Hexahedrons for which the projected centroid is inside the convexhull:

active = findall(h -> shadow(centroid(h))  chull, grid)

blocks = view(grid, active) 
136422 view(::CartesianGrid, [76, 77, 78, 79, ..., 370102, 370103, 370104, 370186])
├─ Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
⋮
├─ Hexahedron((x: -900.005 m, y: 717.1 m, z: 412.58 m), ..., (x: -900.005 m, y: 742.1 m, z: 425.08 m))
├─ Hexahedron((x: -875.005 m, y: 717.1 m, z: 412.58 m), ..., (x: -875.005 m, y: 742.1 m, z: 425.08 m))
├─ Hexahedron((x: -850.005 m, y: 717.1 m, z: 412.58 m), ..., (x: -850.005 m, y: 742.1 m, z: 425.08 m))
├─ Hexahedron((x: -825.005 m, y: 717.1 m, z: 412.58 m), ..., (x: -825.005 m, y: 742.1 m, z: 425.08 m))
└─ Hexahedron((x: -900.005 m, y: 742.1 m, z: 412.58 m), ..., (x: -900.005 m, y: 767.1 m, z: 425.08 m))

We would also like to filter Hexahedrons that are above the terrain. Let’s create a simple terrain elevation model by interpolating the vertical z coordinate of the first point of each trajectory:

zcoord(point) = coords(point).z

ztable = @chain dtable begin
  @groupby(:HOLEID)
  @transform(:z = zcoord(:geometry), :geometry = shadow(:geometry))
  @combine(:z = first(:z), :geometry = first(:geometry))
end
119×3 GeoTable over 119 PointSet
HOLEID z geometry
Categorical Continuous Point
[NoUnits] [m] 🖈 Cartesian{NoDatum}
1 252.82 m (x: 559.725 m, y: -513.31 m)
2 280.94 m (x: 745.345 m, y: -280.6 m)
3 281.09 m (x: -304.025 m, y: 318.13 m)
4 256.16 m (x: -416.575 m, y: 570.35 m)
5 324.65 m (x: 198.815 m, y: -524.78 m)
6 317.1 m (x: 493.605 m, y: -530.83 m)
7 245.13 m (x: -367.965 m, y: 394.78 m)
8 250.67 m (x: 17.6752 m, y: 33.4297 m)
9 254.49 m (x: -514.305 m, y: 594.68 m)
10 294.44 m (x: -674.415 m, y: 596.84 m)

We perform the interpolation of the z coordinate on the projected centroids of the blocks:

centroids = unique(shadow.(centroid.(blocks)))

ztable = ztable |> Select("z") |> Interpolate(centroids, IDW())
2067×2 GeoTable over 2067 PointSet
z geometry
Continuous Point
[m] 🖈 Cartesian{NoDatum}
315.273 m (x: 737.495 m, y: -870.4 m)
318.783 m (x: 762.495 m, y: -870.4 m)
321.816 m (x: 787.495 m, y: -870.4 m)
324.655 m (x: 812.495 m, y: -870.4 m)
327.816 m (x: 837.495 m, y: -870.4 m)
309.922 m (x: 687.495 m, y: -845.4 m)
314.0 m (x: 712.495 m, y: -845.4 m)
318.755 m (x: 737.495 m, y: -845.4 m)
323.911 m (x: 762.495 m, y: -845.4 m)
327.361 m (x: 787.495 m, y: -845.4 m)
ztable |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

Finally, we can filter the blocks for which the z coordinate is below the terrain:

p(h) = shadow(centroid(h))
z(h) = zcoord(centroid(h))

zdict = Dict(ztable.geometry .=> ztable.z)

active = findall(h -> z(h) < zdict[p(h)], blocks)

blocks = view(blocks, active)
110854 view(::CartesianGrid, [76, 77, 78, 79, ..., 337107, 337109, 337193, 342719])
├─ Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
├─ Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
⋮
├─ Hexahedron((x: 924.995 m, y: -682.9 m, z: 337.58 m), ..., (x: 924.995 m, y: -657.9 m, z: 350.08 m))
├─ Hexahedron((x: 874.995 m, y: -757.9 m, z: 350.08 m), ..., (x: 874.995 m, y: -732.9 m, z: 362.58 m))
├─ Hexahedron((x: 924.995 m, y: -757.9 m, z: 350.08 m), ..., (x: 924.995 m, y: -732.9 m, z: 362.58 m))
├─ Hexahedron((x: 899.995 m, y: -732.9 m, z: 350.08 m), ..., (x: 899.995 m, y: -707.9 m, z: 362.58 m))
└─ Hexahedron((x: 924.995 m, y: -757.9 m, z: 362.58 m), ..., (x: 924.995 m, y: -732.9 m, z: 375.08 m))
viz(blocks)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

The filtered blocks constitute our domain of interpolation.

12.3.3 Interpolation of grades

We saw that the distribution of chemical elements in the drill hole samples is very skewed. This is always the case in the mining industry. Another issue is that metal and mineral grades are examples of compositional data (Aitchison 1982). The values in these variables are constrained to live in the interval \([0,1]\) and to sum up to 100% if all chemical elements are considered.

12.3.3.1 Preprocessing

In order to remove compositional data constraints, we will perform the centered log-ratio transform (CLR) from the CoDa.jl module:

grades = dtable |> Select("Cu", "Au", "Ag", "S")

grades |> CLR() |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

After the transform, the variables are free to vary in the unbounded interval \([-\infty,\infty]\). The theory behind this transform is beyond the scope of this book. Nevertheless, it is a simple mathematical expression in terms of logarithms of ratios (e.g., Cu/S).

Next, we attempt to transform the multivariate distribution to a multivariate standard normal using the ProjectionPursuit transform:

grades |> CLR() |> ProjectionPursuit() |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

The ProjectionPursuit is an advanced statistical transform that removes non-linear associations between variables using an iterative procedure (Friedman 1987). The result is a set of independent variables that can be interpolated separately.

In order to “undo” these transforms after the interpolation, we create a revertible pipeline:

preproc = CLR()  ProjectionPursuit()

samples, cache = apply(preproc, grades)

samples
2000×5 GeoTable over 2000 PointSet
PP1 PP2 PP3 PP4 geometry
Continuous Continuous Continuous Continuous Point
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
-0.73792 1.54055 -1.24393 1.23254 (x: 559.725 m, y: -513.31 m, z: 252.82 m)
-1.47815 -0.312571 -1.87602 1.08545 (x: 558.955 m, y: -515.23 m, z: 246.87 m)
-0.256941 0.909628 0.309467 1.24678 (x: 557.225 m, y: -519.61 m, z: 233.37 m)
-1.39146 1.58396 0.081597 -0.384901 (x: 555.375 m, y: -524.3 m, z: 218.92 m)
-0.577091 1.20691 -0.462776 -0.41225 (x: 553.825 m, y: -528.21 m, z: 206.94 m)
-1.08725 1.90232 -0.144696 0.0514477 (x: 552.075 m, y: -532.56 m, z: 193.91 m)
-0.436695 2.0841 -1.81956 1.45479 (x: 550.305 m, y: -537.08 m, z: 180.8 m)
-1.97113 0.382926 -0.31922 0.760317 (x: 547.495 m, y: -544.31 m, z: 160.08 m)
-0.151986 -1.17863 -1.03556 1.3564 (x: 544.335 m, y: -552.43 m, z: 137.08 m)
-1.32445 -0.587836 -0.17002 -0.191781 (x: 541.695 m, y: -559.19 m, z: 117.96 m)

12.3.3.2 Geospatial correlation

Let’s fit a theoretical variogram for all four (independent) variables up to a given maximum lag:

maxlag = 300.0u"m"

ns = setdiff(names(samples), ["geometry"])

gs = [EmpiricalVariogram(samples, n, maxlag = maxlag) for n in ns]

γs = [GeoStatsFunctions.fit(Variogram, g, h -> 1 / h^2) for g in gs]
4-element Vector{Variogram}:
 MaternVariogram(sill: 0.873118, nugget: 0.434436, order: 1.0, range: 164.851 m, distance: Euclidean)
 GaussianVariogram(sill: 0.936908, nugget: 0.639555, range: 97.5219 m, distance: Euclidean)
 MaternVariogram(sill: 0.894849, nugget: 0.53176, order: 1.0, range: 158.631 m, distance: Euclidean)
 ExponentialVariogram(sill: 0.904108, nugget: 0.440981, range: 285.006 m, distance: Euclidean)
Note

We performed the fit of the variogram model using the weighting function h -> exp(-h/100) that penalizes the lag distance h with an exponential model. The constant 100 was chosen based on visual inspection of the EmpiricalVariogram estimates below.

function gammaplot(n, g, γ)
  varioplot(g, axis = (; title = n))
  varioplot!(γ, maxlag = maxlag, color = "teal")
  Mke.current_figure()
end

gammaplot(ns[1], gs[1], γs[1])
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227
gammaplot(ns[2], gs[2], γs[2])
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227
gammaplot(ns[3], gs[3], γs[3])
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227
gammaplot(ns[4], gs[4], γs[4])
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

Assuming that the variogram models are adequate, we can proceed to interpolation.

12.3.3.3 Geostatistical interpolation

Given the domain of interpolation, the samples and the variogram models, we can perform interpolation with InterpolateNeighbors:

models = [n => Kriging(γ) for (n, γ) in zip(ns, γs)]

interp = samples |> InterpolateNeighbors(blocks, models...)
110854×5 GeoTable over 110854 view(::CartesianGrid, [76, 77, 78, 79, ..., 337107, 337109, 337193, 342719])
PP1 PP2 PP3 PP4 geometry
Continuous Continuous Continuous Continuous Hexahedron
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
-0.291375 -0.280342 0.257005 0.431866 Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
0.102569 -0.192954 0.653183 0.474708 Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
0.102561 -0.192954 0.653191 0.474365 Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
0.102555 -0.192954 0.653196 0.474068 Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
0.0279725 -0.151112 0.122258 -0.13861 Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
-0.363449 -0.0632676 0.398047 0.450177 Hexahedron((x: 674.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 674.995 m, y: -832.9 m, z: -387.42 m))
-0.291377 -0.280342 0.25701 0.432448 Hexahedron((x: 699.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 699.995 m, y: -832.9 m, z: -387.42 m))
-0.291369 -0.280342 0.257037 0.432079 Hexahedron((x: 724.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -832.9 m, z: -387.42 m))
0.102552 -0.192954 0.6532 0.474799 Hexahedron((x: 749.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -832.9 m, z: -387.42 m))
0.102542 -0.192954 0.653209 0.474406 Hexahedron((x: 774.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -832.9 m, z: -387.42 m))

Let’s confirm that the interpolated values follow the same standard normal distribution:

interp |> Sample(10000) |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

12.3.3.4 Postprocessing

In order to get the interpolated values in the original compositional space, we need to revert the preprocessing pipeline:

estim = revert(preproc, interp, cache)
110854×5 GeoTable over 110854 view(::CartesianGrid, [76, 77, 78, 79, ..., 337107, 337109, 337193, 342719])
Cu Au Ag S geometry
Continuous Continuous Continuous Continuous Hexahedron
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
0.741654 3.1018e-5 0.000199469 0.258115 Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
0.74906 2.50579e-5 0.000207719 0.250707 Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
0.749094 2.51114e-5 0.000208065 0.250673 Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
0.74909 2.51123e-5 0.000208064 0.250677 Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
0.752767 3.34579e-5 0.00019871 0.247 Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
0.740595 3.16098e-5 0.000204964 0.259168 Hexahedron((x: 674.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 674.995 m, y: -832.9 m, z: -387.42 m))
0.741626 3.10167e-5 0.000199443 0.258143 Hexahedron((x: 699.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 699.995 m, y: -832.9 m, z: -387.42 m))
0.741632 3.10172e-5 0.000199465 0.258138 Hexahedron((x: 724.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -832.9 m, z: -387.42 m))
0.749062 2.50567e-5 0.000207718 0.250706 Hexahedron((x: 749.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -832.9 m, z: -387.42 m))
0.749094 2.51113e-5 0.000208066 0.250673 Hexahedron((x: 774.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -832.9 m, z: -387.42 m))
estim |> Select("Cu") |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

12.3.4 Model of recovery

We introduce a simplistic model of metallurgical recovery using the grade of copper estimated at the mining blocks. We assume that the logistic function represents an ideal behavior for the recovery as the grade of copper increases:

μ = mean(estim.Cu) - 0.1
σ = std(estim.Cu)

f(Cu) = 1 / (1 + exp(-(Cu - μ) / σ))
f (generic function with 1 method)
estim = estim |> Map("Cu" => f => "f")
110854×6 GeoTable over 110854 view(::CartesianGrid, [76, 77, 78, 79, ..., 337107, 337109, 337193, 342719])
Cu Au Ag S f geometry
Continuous Continuous Continuous Continuous Continuous Hexahedron
[NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
0.741654 3.1018e-5 0.000199469 0.258115 0.987889 Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
0.74906 2.50579e-5 0.000207719 0.250707 0.991279 Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
0.749094 2.51114e-5 0.000208065 0.250673 0.991292 Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
0.74909 2.51123e-5 0.000208064 0.250677 0.991291 Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
0.752767 3.34579e-5 0.00019871 0.247 0.992604 Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
0.740595 3.16098e-5 0.000204964 0.259168 0.987307 Hexahedron((x: 674.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 674.995 m, y: -832.9 m, z: -387.42 m))
0.741626 3.10167e-5 0.000199443 0.258143 0.987873 Hexahedron((x: 699.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 699.995 m, y: -832.9 m, z: -387.42 m))
0.741632 3.10172e-5 0.000199465 0.258138 0.987877 Hexahedron((x: 724.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -832.9 m, z: -387.42 m))
0.749062 2.50567e-5 0.000207718 0.250706 0.99128 Hexahedron((x: 749.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -832.9 m, z: -387.42 m))
0.749094 2.51113e-5 0.000208066 0.250673 0.991293 Hexahedron((x: 774.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -832.9 m, z: -387.42 m))

Please check the paper by Hoffimann et al. (2022b) for a more elaborate model of metallurgical recovery in the locked-cycle-test.

12.3.5 Economic assessment

Given the block model with the grade of copper and metallurgical recovery, we can proceed and apply the formula of economic value stated in our objectives:

ton = 1000u"kg"

ρ = 2.75 * ton / 1u"m^3"
P = 4000 / ton
Cₘ = 4 / ton
Cₚ = 10 / ton

estim = @transform(estim,
  :value = volume(:geometry) * ρ * ((:Cu / 100) * :f * P - (Cₘ + Cₚ))
)
110854×7 GeoTable over 110854 view(::CartesianGrid, [76, 77, 78, 79, ..., 337107, 337109, 337193, 342719])
Cu Au Ag S f value geometry
Continuous Continuous Continuous Continuous Continuous Continuous Hexahedron
[NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
0.741654 3.1018e-5 0.000199469 0.258115 0.987889 3.28859e5 Hexahedron((x: 724.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -857.9 m, z: -387.42 m))
0.74906 2.50579e-5 0.000207719 0.250707 0.991279 3.37328e5 Hexahedron((x: 749.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -857.9 m, z: -387.42 m))
0.749094 2.51114e-5 0.000208065 0.250673 0.991292 3.37366e5 Hexahedron((x: 774.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -857.9 m, z: -387.42 m))
0.74909 2.51123e-5 0.000208064 0.250677 0.991291 3.37361e5 Hexahedron((x: 799.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 799.995 m, y: -857.9 m, z: -387.42 m))
0.752767 3.34579e-5 0.00019871 0.247 0.992604 341344.0 Hexahedron((x: 824.995 m, y: -882.9 m, z: -399.92 m), ..., (x: 824.995 m, y: -857.9 m, z: -387.42 m))
0.740595 3.16098e-5 0.000204964 0.259168 0.987307 3.27589e5 Hexahedron((x: 674.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 674.995 m, y: -832.9 m, z: -387.42 m))
0.741626 3.10167e-5 0.000199443 0.258143 0.987873 3.28825e5 Hexahedron((x: 699.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 699.995 m, y: -832.9 m, z: -387.42 m))
0.741632 3.10172e-5 0.000199465 0.258138 0.987877 3.28832e5 Hexahedron((x: 724.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 724.995 m, y: -832.9 m, z: -387.42 m))
0.749062 2.50567e-5 0.000207718 0.250706 0.99128 3.3733e5 Hexahedron((x: 749.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 749.995 m, y: -832.9 m, z: -387.42 m))
0.749094 2.51113e-5 0.000208066 0.250673 0.991293 3.37366e5 Hexahedron((x: 774.995 m, y: -857.9 m, z: -399.92 m), ..., (x: 774.995 m, y: -832.9 m, z: -387.42 m))

We can then visualize all blocks with a positive economic value:

estim |> Filter(x -> x.value > 0) |> Select("value") |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

Or any criterion of interest such as positive economic value and small fraction of contaminants:

estim |> Filter(x -> x.value > 0 && x.S < 0.25) |> Select("value") |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/We6MY/src/scenes.jl:227

12.4 Summary

In this chapter, we illustrated an application of the framework in the mining industry. Among other things, we learned how to

  • Perform simple economic assessment based on grades and metallurgical recoveries estimated at mining blocks using simple interpolation of transformed variables from drill hole samples.
  • Use the tools covered in previous chapters to localize regions of interest in the mineral deposit.

Although the mathematical model presented here is simple, it is what most mining companies do. There is opportunity to improve these types of estimates with more sophisticated geospatial data science pipelines.