# framework
using GeoStats
# IO modules
using GeoIO
# viz modules
using PairPlots
import CairoMakie as Mke
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:
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:
= "https://zenodo.org/record/7051975/files/drillholes.csv?download=1"
url
= download(url, tempname()*".csv")
csv
= GeoIO.load(csv, coords = ("X", "Y", "Z"))
dtable
|> Select("Cu ppm") |> viewer dtable
┌ 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
|> describe dtable
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.
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:
- Preliminary analysis and processing
- Definition of interpolation domain
- Multivariate geostatistical interpolation
- 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:
= Select("HOLEID")
selectholeid
= Select("Cu ppm" => "Cu",
selectgrades "Au ppm" => "Au",
"Ag ppm" => "Ag",
"S ppm" => "S") →
Functional(x -> 1e-4*x*u"percent") # 1 ppm = 1e-4 percent
= selectholeid ⊔ selectgrades dclean
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)
= dclean(dtable) dtable
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
:
|> Select("Cu", "Au", "Ag", "S") |> values |> pairplot dtable
┌ 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
= boundingbox(dtable.geometry)
bbox
# size of blocks in meters
= (25.0u"m", 25.0u"m", 12.5u"m")
bsize
# define Cartesian grid
= CartesianGrid(extrema(bbox)..., bsize) grid
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)
current_figure() Mke.
┌ 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")
= shadow.(dtable.geometry)
points
= convexhull(points) chull
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")
current_figure() Mke.
┌ 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 Hexahedron
s for which the projected centroid is inside the convexhull
:
= findall(h -> shadow(centroid(h)) ∈ chull, grid)
active
= view(grid, active) blocks
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 Hexahedron
s 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
= @chain dtable begin
ztable @groupby(:HOLEID)
@transform(:z = zcoord(:geometry), :geometry = shadow(:geometry))
@combine(:z = first(:z), :geometry = first(:geometry))
end
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:
= unique(shadow.(centroid.(blocks)))
centroids
= ztable |> Select("z") |> Interpolate(centroids, IDW()) ztable
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) |
⋮ | ⋮ |
|> viewer ztable
┌ 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