Skip to content

Note

Click here to download the full example code

runs and strip logs

grade control asks where each hole is ore: the contiguous runs of samples above a cutoff, from and to, with their length-weighted grade. raw runs follow each sample, but a mine cannot dig a 1 m pod of ore or skip a 1 m band of waste. Drillholes.runs cleans them with three rules: internal dilution takes short waste bands into the ore around them, an edge skin adds waste on each side of each ore run, and a minimum mining length merges short runs into their neighbors. bt.plot.strip_log draws the result down a hole, beside the lithology and the grade.

import sys
from pathlib import Path

HERE = Path(__file__).parent if "__file__" in globals() else Path.cwd()
sys.path.insert(0, str(HERE.parents[1]))
import boitata as bt
import matplotlib.pyplot as plt
import numpy as np
from common import ACCENT, GRAY, HIGHLIGHT, LIGHT, save

data = bt.datasets.vein_gold_grade_control()
kind = dict(zip(data["collars"]["HOLE_ID"], data["collars"]["TYPE"], strict=True))
intervals = bt.merge_intervals(data["assays"], data["lithology"])
intervals = intervals.filter(np.array([kind[h] == "DD" for h in intervals["HOLE_ID"]]))
holes = bt.Drillholes(data["collars"], data["surveys"], intervals)
length = intervals["TO"] - intervals["FROM"]
metal = np.nansum(intervals["AU_GPT"] * length)
print(f"{np.sum(length[~np.isnan(intervals['AU_GPT'])]):.0f} m of assayed core, {metal:.0f} g/t x m of gold")

Out:

40830 m of assayed core, 5367 g/t x m of gold

runs above a cutoff

at 0.5 g/t each assay is ore or waste, and each change of flag down a hole starts a new run. each rule then takes the runs of the previous line: up to 3 m of internal dilution, a skin of 0.5 m at each edge, a minimum length of 2 m. the runs of a hole partition its assays, so Σ grade × length over all runs gives back the assay metal for each rule. only the share that lands in ore changes.

CUTOFF = 0.5
rules = {
    "raw": {},
    "+ dilution 3 m": {"max_dilution": 3.0},
    "+ edges 0.5 m": {"max_dilution": 3.0, "edge": 0.5},
    "+ minimum 2 m": {"max_dilution": 3.0, "edge": 0.5, "min_length": 2.0},
}
runs = {name: holes.runs("AU_GPT", cutoff=CUTOFF, **kw) for name, kw in rules.items()}
print(f"{'':16}{'ore runs':>9}{'ore (m)':>9}{'Au (g/t)':>9}{'metal in ore':>13}{'balance':>9}")
for name, r in runs.items():
    ore, L, au = r["ore"], r["length"], r["AU_GPT"]
    total = np.dot(L, au)
    print(
        f"{name:16}{ore.sum():9}{L[ore].sum():9.0f}{np.dot(L[ore], au[ore]) / L[ore].sum():9.2f}"
        f"{100 * np.dot(L[ore], au[ore]) / metal:12.1f}%{total - metal:9.1e}"
    )

Out:

                 ore runs  ore (m) Au (g/t) metal in ore  balance
raw                   337      811     5.11        77.2% -9.1e-13
+ dilution 3 m        240      918     4.54        77.6% -9.1e-13
+ edges 0.5 m         238     1157     3.63        78.2%  0.0e+00
+ minimum 2 m         230     1144     3.65        77.9%  0.0e+00

dilution joins raw runs into fewer, longer ones at a lower grade, and the metal in ore barely moves because the waste it takes in is lean. the edge skins add a meter of waste to each run, where most of the tonnage grows. a 1 m pod that reaches 2 m with its skins then survives the minimum length. the minimum length drops the pods that stay shorter and fills the waste gaps shorter than 2 m.

internal dilution by hand

hole UD0030 cuts vein V1 as 1 m of ore, 2 m of waste, then 2 m of ore:

raw = runs["raw"]
near = (raw["HOLE_ID"] == "UD0030") & (raw["from"] > 157) & (raw["to"] < 164)
for f, t, au, ore in zip(
    raw["from"][near], raw["to"][near], raw["AU_GPT"][near], raw["ore"][near], strict=True
):
    print(f"{f:7.2f} {t:7.2f} {au:5.2f} g/t {'ore' if ore else 'waste'}")
L = (raw["to"] - raw["from"])[near]
by_hand = np.dot(L, raw["AU_GPT"][near]) / L.sum()
diluted = runs["+ dilution 3 m"]
row = np.flatnonzero((diluted["HOLE_ID"] == "UD0030") & (diluted["from"] == 158.25))[0]
print(f"by hand: {by_hand:.3f} g/t over {L.sum():.2f} m")
print(f"runs:    {diluted['AU_GPT'][row]:.3f} g/t over {diluted['length'][row]:.2f} m")

Out:

 158.25  159.25  1.88 g/t ore
 159.25  161.25  0.21 g/t waste
 161.25  163.25  4.81 g/t ore
by hand: 2.382 g/t over 5.00 m
runs:    2.382 g/t over 5.00 m

the waste band spans 2 m, within the 3 m allowed, and the three runs together grade 2.38 g/t, above the cutoff, so the rule takes it in. had the combined grade fallen below 0.5 g/t, the two ore runs would have stayed apart.

strip logs

three holes through the veins, each with its lithology, vein, gold and final ore runs. gold is drawn as a step per assay, from zero at the left of its track to the highest assay of the holes drawn at the right.

final = runs["+ minimum 2 m"]
lith = bt.Categories(["AND", "BX", "QV", "RHY"], colors=[LIGHT, GRAY, HIGHLIGHT, "#f2e0c9"])
vein = bt.Categories(["V1", "V2", "V3", "V4"])
windows = {"UD0030": (150, 170), "UD0037": (258, 282), "UD0044": (114, 136)}
fig, axes = plt.subplots(1, 3, figsize=(10, 4.8), layout="constrained")
for ax, (hole, (top, bottom)) in zip(axes, windows.items(), strict=True):
    bt.plot.strip_log(
        holes,
        hole,
        columns=["AU_GPT"],
        categories=["LITH", "VEIN"],
        scheme={"LITH": lith, "VEIN": vein},
        runs=final,
        ax=ax,
        color=ACCENT,
    )
    ax.set_ylim(bottom, top)
    if ax is not axes[-1]:
        ax.get_legend().remove()
    if ax is not axes[0]:
        ax.set_ylabel("")
save(fig, "strip_logs")

example 02 13

each ore run covers the quartz vein and the gold-bearing breccia around it, with the lean assays between them taken in as internal dilution. in UD0044 veins V4 and V2 touch and one intercept spans both. the shading starts half a meter above the first ore assay and ends half a meter below the last: the skins.

ore intercepts

each ore run of the final line is one intercept: its length and grade against those of the raw runs, and the five with the most metal.

fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
for name, color, marker in [("raw", GRAY, "o"), ("+ minimum 2 m", ACCENT, "s")]:
    r = runs[name]
    ore = r["ore"]
    ax.scatter(r["to"][ore] - r["from"][ore], r["AU_GPT"][ore], s=12, color=color, marker=marker, label=name)
ax.axhline(CUTOFF, color=GRAY, lw=0.6, ls=":")
ax.set(xscale="log", yscale="log", xlabel="Intercept length (m)", ylabel="Au (g/t)")
ax.legend()
save(fig, "intercepts")

ore = final["ore"]
below = ore & (final["AU_GPT"] < CUTOFF)
print(f"{below.sum()} final intercepts grade below {CUTOFF} g/t")
top = np.argsort(-(final["length"] * final["AU_GPT"])[ore])[:5]
print(f"{'hole':8}{'from':>8}{'to':>8}{'m':>6}{'Au g/t':>8}")
for k in np.flatnonzero(ore)[top]:
    print(
        f"{final['HOLE_ID'][k]:8}{final['from'][k]:8.2f}{final['to'][k]:8.2f}"
        f"{final['length'][k]:6.1f}{final['AU_GPT'][k]:8.2f}"
    )

example 02 13

Out:

26 final intercepts grade below 0.5 g/t
hole        from      to     m  Au g/t
UD0025    322.25  335.25  13.0   17.03
UD0064    165.75  172.50   6.8   30.24
UD0044    120.00  130.88  10.9   15.30
UD0019    374.50  386.00  11.5   14.40
UD0059    182.25  193.25  11.0   14.71

no raw intercept is shorter than one assay, and each final one spans at least 2 m. the pods under 1 m are gone or grown by their skins to 2 m and more. at 3 m, a column of intercepts marks the 2 m pods with their two skins. the skins cost grade: a lean pod diluted by 1 m of waste can end below the cutoff, and those intercepts deserve a second look before they go to the mine plan.

runs of a category

with category= and ore= the flag comes from a column instead of a cutoff, here the quartz vein. the same rules apply, but without a cutoff internal dilution would take in any short band, so only the minimum length applies.

for min_length in [0.0, 2.0]:
    qv = holes.runs("AU_GPT", category="LITH", ore=["QV"], min_length=min_length)
    ore = qv["ore"]
    L, au = qv["length"][ore], qv["AU_GPT"][ore]
    print(
        f"minimum {min_length:.0f} m: {ore.sum()} quartz vein runs, {L.sum():.0f} m at {np.dot(L, au) / L.sum():.2f} g/t"
    )

Out:

minimum 0 m: 206 quartz vein runs, 436 m at 7.80 g/t
minimum 2 m: 93 quartz vein runs, 308 m at 7.88 g/t

half of the quartz vein runs are thinner than 2 m. at a 2 m minimum they fall to waste, and the rest take in the thin waste bands between vein runs: fewer, thicker runs, at about the same grade.

Total running time of the script: ( 0 minutes 0.703 seconds)

Download Python source code: example_02_13.py

Download Jupyter notebook: example_02_13.ipynb

Gallery generated by mkdocs-gallery