Note
Click here to download the full example code
checking drill holes¶
the stacked sulphide lenses as logged: collar, survey (dip positive down, azimuth clockwise from north), assay and
lithology tables, with typical data-entry errors planted in them and listed in the dataset's raw/README.md. you
check the tables, repair them and check them again before desurveying anything.
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, save
check_drillholes flags each record of the collar, survey and interval tables: duplicate ids, missing or sentinel
values, text in numeric columns, from ≥ to, gaps, overlaps, angles out of range, depths past the collar LENGTH,
ids that match a collar only once trimmed and case-folded, holes missing from a table, abrupt survey deviation and
holes dipping against the rest. the report lists each flagged check with the holes it hit.
data = bt.datasets.stacked_sulphide_lenses(raw=True)
raw = {
"collar": data["collars"],
"survey": data["surveys"],
"assays": data["assays"],
"lithology": data["lithology"],
}
for name, table in raw.items():
print(f"{name:>9}: {table.num_rows:6} rows")
def check(t, show=True):
flags, summary, details = bt.check_drillholes(
t["collar"], t["survey"], {"assays": t["assays"], "lithology": t["lithology"]}, max_depth="LENGTH"
)
for table, name, rows, holes in zip(
summary["table"], summary["check"], summary["rows"], summary["holes"]
):
if rows and show:
ids = t[table]["HOLE_ID"]
hit = sorted(set(ids[flags[table][name]]))
listed = ", ".join(repr(h) for h in hit) if len(hit) <= 8 else f"{hit[0]!r} ... {hit[-1]!r}"
print(f"{table:>9} {name:<12} {rows:4.0f} rows in {holes:3.0f} holes: {listed}")
return flags, details
flags, details = check(raw)
Out:
collar: 290 rows
survey: 4326 rows
assays: 16907 rows
lithology: 1726 rows
collar duplicate 2 rows in 2 holes: 'DD0058', 'DD0067'
collar no_survey 1 rows in 1 holes: 'DD0151'
collar no_assays 112 rows in 112 holes: 'DD0001' ... 'DD0244'
survey deviation 2 rows in 1 holes: 'DD0197'
survey dip_sign 28 rows in 1 holes: 'DD0116'
survey past_depth 5 rows in 1 holes: 'DD0055'
survey id_mismatch 13 rows in 1 holes: 'DD0104'
survey no_collar 8 rows in 1 holes: 'DD0111'
assays inverted 1 rows in 1 holes: 'DD0100'
assays gap 10 rows in 8 holes: 'DD0062', 'DD0080', 'DD0100', 'DD0200', 'RC0008', 'RC0014', 'RC0016', 'RC0031'
assays overlap 5 rows in 2 holes: 'DD0162', 'DD0200'
assays past_depth 4 rows in 1 holes: 'DD0055'
assays sentinel 5 rows in 1 holes: 'DD0110'
assays text_values 3 rows in 1 holes: 'DD0083'
assays id_mismatch 109 rows in 2 holes: 'DD0104', 'dd0062'
assays no_collar 127 rows in 1 holes: 'DD0111'
lithology gap 1 rows in 1 holes: 'DD0132'
lithology past_depth 2 rows in 1 holes: 'DD0055'
lithology id_mismatch 10 rows in 2 holes: 'DD0104', 'DD0132 '
lithology no_collar 7 rows in 1 holes: 'DD0111'
each flag points at a planted error, or follows from one:
duplicatecollars:DD0067entered twice with different coordinates,DD0058the same row twice.id_mismatch:'dd0062'in the assays and'DD0132 'in the lithology are ids in the wrong case or with a trailing space. the collar'DD0104 'has the trailing space, so its survey, assays and lithology miss it.no_collar:DD0111has surveys, assays and lithology but no collar.no_survey:DD0151has no survey.deviation: two stations ofDD0197, where the azimuth flips at one station and back at the next.dip_sign:DD0116has its dips entered negative, pointing up, while the other holes dip down.past_depth:DD0055has surveys, assays and lithology below its collarLENGTH.inverted: an assay ofDD0100withFROM>TO.overlap: two assays ofDD0200start before the previous one ends, and three ofDD0162are entered twice.gap: two missing samples inDD0080. the rows above leave the gaps inDD0062,DD0100,DD0132andDD0200. the gaps in the RC holes are unsampled core and real.text_valuesandsentinel: the reader took the grades as text, because some cells are not numbers.DD0110holds-99and-999.no_assays: 112 collars. only mineralized zones are assayed, so a hole that never logsMS,SMSorSTRhas no assays by design. one more hides among them.
the details name the value behind each id, dip and text flag, and what it should become:
groups = {}
for table, check_, hole, column, value, suggestion in zip(
*(details[c] for c in ("table", "check", "hole", "column", "value", "suggestion"))
):
groups.setdefault((table, check_, hole, column), set()).add(
f"{value!r} -> {suggestion!r}" if suggestion else repr(value)
)
for (table, check_, hole, column), values in groups.items():
values = sorted(values)
listed = ", ".join(values) if len(values) <= 2 else f"{values[0]} ... {values[-1]} ({len(values)} values)"
print(f"{table:>9} {check_:<12} {hole!r:9} {column:<7} {listed}")
Out:
survey dip_sign 'DD0116' DIP '-53.8' -> '53.8' ... '-63.81' -> '63.81' (28 values)
assays text_values 'DD0083' ZN_PCT 'NS'
assays text_values 'DD0083' PB_PCT 'NS'
assays text_values 'DD0083' CU_PCT '<0.01', 'NS'
assays text_values 'DD0083' AG_GPT 'NS'
assays text_values 'DD0083' AU_GPT '<0.01', 'NS'
survey id_mismatch 'DD0104' HOLE_ID 'DD0104' -> 'DD0104 '
assays id_mismatch 'dd0062' HOLE_ID 'dd0062' -> 'DD0062'
assays id_mismatch 'DD0104' HOLE_ID 'DD0104' -> 'DD0104 '
lithology id_mismatch 'DD0104' HOLE_ID 'DD0104' -> 'DD0104 '
lithology id_mismatch 'DD0132 ' HOLE_ID 'DD0132 ' -> 'DD0132'
a hole missing its assays looks like one of the unassayed holes, unless its lithology says it is mineralized:
lith = raw["lithology"]
mineralized = set(lith["HOLE_ID"][np.isin(lith["LITH"], ["MS", "SMS", "STR"])])
print("mineralized, no assays:", sorted(mineralized - set(raw["assays"]["HOLE_ID"])))
Out:
mineralized, no assays: ['DD0043']
two repairs need a person. you swap the ends of the inverted interval and take DD0055's LENGTH from its
deepest survey station, since the survey, assays and lithology agree the hole is longer. after a second check,
fix_drillholes resolves the rest with one named rule per check. two rules are asked for by name:
dip_sign="negate" turns DD0116 down, and text_values="half" reads <0.01 as half the detection limit
(by default it becomes null, as NS does). the log says what each rule changed: ids are renamed to their collar,
the later duplicate collar goes (for DD0067, the second, wrong entry), overlapping assays keep the one that
starts first (dropping the twinned re-entries), the two DD0197 stations turned by the flip go, sentinels become
null, and the rows of a hole without collar go.
def replace(table, **columns):
return bt.Table({**{c: table[c] for c in table.column_names}, **columns})
tables = dict(raw)
a, s, c = raw["assays"], raw["survey"], raw["collar"]
swap = a["FROM"] > a["TO"]
tables["assays"] = replace(a, FROM=np.where(swap, a["TO"], a["FROM"]), TO=np.where(swap, a["FROM"], a["TO"]))
deepest = {}
for h, d in zip(s["HOLE_ID"], s["DEPTH"]):
deepest[h] = max(deepest.get(h, 0.0), d)
tables["collar"] = replace(c, LENGTH=np.maximum(c["LENGTH"], [deepest.get(h, 0.0) for h in c["HOLE_ID"]]))
flags, _ = check(tables, show=False)
fixed, log = bt.fix_drillholes(flags, tables, dip_sign="negate", text_values="half")
for table, name, action, rows in zip(log["table"], log["check"], log["action"], log["rows"]):
if rows:
print(f"{action:>10} {rows:3.0f} {table} rows flagged {name}")
print()
flags, _ = check(fixed)
Out:
drop 2 collar rows flagged duplicate
drop 2 survey rows flagged deviation
drop 8 survey rows flagged no_collar
negate 28 survey rows flagged dip_sign
rename 13 survey rows flagged id_mismatch
keep_first 5 assays rows flagged overlap
half 3 assays rows flagged text_values
null 5 assays rows flagged sentinel
drop 127 assays rows flagged no_collar
rename 109 assays rows flagged id_mismatch
drop 7 lithology rows flagged no_collar
rename 10 lithology rows flagged id_mismatch
collar no_survey 1 rows in 1 holes: 'DD0151'
collar no_assays 112 rows in 112 holes: 'DD0001' ... 'DD0244'
assays gap 8 rows in 6 holes: 'DD0080', 'DD0200', 'RC0008', 'RC0014', 'RC0016', 'RC0031'
the rest needs the field records, since no rule can fix it. DD0151 has no survey and would be desurveyed as
vertical, DD0043 needs its assays found, and the gaps are the two lost DD0080 samples, the two DD0200
intervals dropped as overlaps and the unsampled RC core. 'DD0104 ' keeps its trailing space, now in every table.
DD0116 shows why the dips matter: desurveyed as entered, it climbs out of the ground.
def dd0116(table):
return table.filter(table["HOLE_ID"] == "DD0116")
collar = dd0116(fixed["collar"])
fig, ax = plt.subplots(figsize=(4, 5.5), layout="constrained")
for name, survey, color in (("as entered", raw["survey"], HIGHLIGHT), ("fixed", fixed["survey"], ACCENT)):
path = bt.Drillholes(collar, dd0116(survey)).paths()
ax.plot(path["x"], path["z"], color=color, lw=1.6, label=name)
ax.axhline(collar["Z"][0], color=GRAY, lw=0.6, ls="--", label="collar elevation")
ax.set_aspect("equal")
ax.set(title="DD0116, east-west section", xlabel="Easting (m)", ylabel="Elevation (m)")
ax.legend()
save(fig, "dip")

Total running time of the script: ( 0 minutes 0.151 seconds)
Download Python source code: example_02_01.py