geoml.data.drillhole

An interval table gives each of its columns a role, which decides how a composite is formed, and may give it a unit — units={"Pb": "%", "Ag": "ppm"} at construction, or afterwards through IntervalTable.set_unit(column, unit) for one column and DrillholeData.set_unit(table, {column: unit}) for a table at a time. The unit travels with the column through renaming and compositing and lands on the variable the conversion builds, where it names an axis and, for a part of a composition, converts the part to a fraction of the whole at the model’s door.

class geoml.data.drillhole.DrillholeData(collar, survey=None, hole='HOLEID', x='X', y='Y', z='Z', length=None, dip=None, azimuth=None, depth='DEPTH', dip_positive_down=True)[source]

Bases: _SpatialData

Drillhole data: a set of collars, their traces, and any number of interval tables.

This object is not fed to a model. It is a staging area in which the raw files are checked, desurveyed and composited, and from which point data is produced with as_point_data() or as_classification_input().

The hole path is computed by minimum curvature from the survey table. Holes with no survey are taken as straight, following the collar dip and azimuth, or vertical if those are absent.

collar

Indexed by hole, with the X, Y and Z coordinates, the hole LENGTH, and the fallback DIP and AZIMUTH.

Type:

DataFrame

intervals

Name to IntervalTable.

Type:

dict

__init__(collar, survey=None, hole='HOLEID', x='X', y='Y', z='Z', length=None, dip=None, azimuth=None, depth='DEPTH', dip_positive_down=True)[source]

Initializer for DrillholeData.

Parameters:
  • collar (DataFrame) – One row per hole, with its coordinates.

  • survey (DataFrame) – Downhole survey stations. Optional.

  • hole (str) – Column with the hole name, in every table.

  • x (str) – Columns in collar with the coordinates.

  • y (str) – Columns in collar with the coordinates.

  • z (str) – Columns in collar with the coordinates.

  • length (str) – Column in collar with the total length of the hole. Optional, but needed to check intervals running past the end of a hole.

  • dip (str) – Columns with the attitude, in collar (as a fallback for holes with no survey) and in survey.

  • azimuth (str) – Columns with the attitude, in collar (as a fallback for holes with no survey) and in survey.

  • depth (str) – Column in survey with the depth of each station.

  • dip_positive_down (bool) – Whether a positive dip points downwards, as in the rest of geoML. Databases recording downward holes as negative need False here.

  • add_intervals(). (Interval tables are added afterwards with)

property n_holes
classmethod from_geoh5(workspace, name=None)[source]

Reads drillholes from a geoh5 workspace.

Every hole becomes a collar row named as the file names it, its survey stations come across with geoh5’s own dip convention converted — there a vertical downward hole reads -90, so the object is built with dip_positive_down=False — and each named property group holding FROM/TO columns becomes one interval table of that name, gathered across the holes. Referenced (coded) columns take the categorical role; depth-associated readings, which cover no interval, are left out. name narrows the read to one drillhole group, or to one hole.

Needs the geoh5py package: pip install geoml[geoh5].

Parameters:
  • workspace (_types.PathLike | _GeoH5Workspace) – Path of the workspace to read, or an open geoml.data.geoh5.Workspace.

  • name (str | None) – A drillhole group, or a single hole, when the file holds more than one.

Returns:

data (DrillholeData) – Ready for validation, compositing and conversion, like any other drillhole database.

Raises:

ValueError – If the workspace holds no drillholes, or none of the given name.

Return type:

DrillholeData

add_intervals(name, data, on_error='warn', **kwargs)[source]

Adds an interval table.

Parameters:
  • name (str) – A label for the table, used to refer to it later.

  • data (DataFrame or IntervalTable) – The interval file. A data frame is passed to IntervalTable along with the remaining arguments.

  • on_error (str) – Passed to IntervalTable.validate(): “warn”, “raise” or “ignore”.

Returns:

self, so that calls can be chained.

Return type:

DrillholeData

rename(table, columns)[source]

Renames columns of one of the interval tables.

The roles travel with the columns; see IntervalTable.rename(). This is the way to tidy the names of a database that was built for you — by datasets.macpass(), say — where the data frames were never in reach.

Parameters:
  • table (str) – Name of the interval table.

  • columns (dict) – Maps each current column name to its new one.

Returns:

self, so that calls can be chained.

set_unit(table, units)[source]

Declares what the columns of one interval table are measured in.

Several at a time, which is how they are usually read off an assay certificate; see IntervalTable.set_unit() for what a unit is worth. Declare them before compositing: the units travel with the columns into whatever a composite or a subset returns, but those are new objects, so a declaration made on one does not reach the other.

Parameters:
  • table (str) – Name of the interval table.

  • units (dict) – Maps each column to its unit – "%", "ppm", "g/t", or a number to divide by. None takes a declaration back.

Returns:

self, so that calls can be chained.

Return type:

DrillholeData

rename_table(name, new_name)[source]

Renames an interval table, keeping its position.

Parameters:
  • name (str) – The table’s current name.

  • new_name (str) – What to call it instead.

Returns:

self, so that calls can be chained.

Return type:

DrillholeData

drop_table(name)[source]

Removes an interval table.

Parameters:

name (str) – The table to remove.

Returns:

self, so that calls can be chained.

Return type:

DrillholeData

validate(on_error='warn')[source]

Validates every interval table. Returns a combined report.

coordinates_at(holes, depths)[source]

The coordinates of the given depths in the given holes.

Parameters:
  • holes (array) – Hole name of each point.

  • depths (array) – Depth of each point along its hole.

Returns:

coordinates (array) – An (n_points, 3) array. Holes with no collar give NaN.

Return type:

ndarray

composite(length=1.0, domain=None)[source]

Composites every interval table onto a common support.

Runs of the given length are laid out along each hole and every table is aggregated onto them, so the tables come out sharing one support and can be merged row for row by as_point_data().

Numeric columns are averaged weighted by length, or by length times density where the table declares a density column, skipping missing values. Categories are decided by the greatest length within the run. A run shorter than length is kept whole and the residual of a longer one is merged into its last run, so no stubs are produced.

Parameters:
  • length (float) – The length of the composites.

  • domain (str or tuple) – A categorical column whose boundaries the composites must honour, given as a table name, a column name, or a (table, column) pair. Without it the runs ignore geology, which smears contacts.

Returns:

drillholes (DrillholeData) – A new object with the same collars and traces.

Return type:

DrillholeData

composite_fixed(length=1.0)[source]

Composites onto runs of fixed length, ignoring any geology.

The same as composite() with no domain; kept as a separate name because ignoring domain boundaries is a deliberate choice.

Return type:

DrillholeData

composite_to(table)[source]

Composites every table onto the intervals of one of them.

This is the way to bring assays onto the geological intervals that were logged, rather than onto an arbitrary run length.

Parameters:

table (str or IntervalTable) – The table whose intervals become the support.

Return type:

DrillholeData

merge_domains(domain)[source]

Merges touching intervals sharing a category into single runs.

Parameters:

domain (str or tuple) – The categorical column, as in composite().

Returns:

table (IntervalTable) – A table with one interval per run.

subset_holes(holes)[source]

The given holes only, with everything logged in them.

Parameters:

holes (str or list) – The names of the holes to keep.

Returns:

drillholes (DrillholeData) – A new object with the selected collars, their traces, and every interval table cut down to them.

subset_region(min_val, max_val)[source]

The holes whose collar falls within a bounding box.

The box is applied to the collars, not to the individual intervals, so a hole is kept or dropped whole: a deviated hole that wanders out of the box still comes through entire. Both bounds are inclusive.

Parameters:
  • min_val (array) – Opposite corners of the box, as [x, y] or [x, y, z]. Two values select in plan view, which is the usual way to carve out an area. The min and max of a BoundingBox can be passed directly.

  • max_val (array) – Opposite corners of the box, as [x, y] or [x, y, z]. Two values select in plan view, which is the usual way to carve out an area. The min and max of a BoundingBox can be passed directly.

Returns:

drillholes (DrillholeData)

filter_intervals(table, column, values, whole_holes=False)[source]

The intervals whose column holds one of the given values.

Parameters:
  • table (str) – The interval table to filter.

  • column (str) – The column to look at.

  • values (object or list) – The values to keep.

  • whole_holes (bool) – Whether to keep every hole containing a matching interval, with all of its data, rather than the matching intervals alone. Cutting the intervals themselves leaves the tables on different supports, so the result has to be composited again before as_point_data(); with whole_holes nothing is cut but the holes.

Returns:

drillholes (DrillholeData)

category_legend(domain)[source]

The distinct values of a categorical column, and how much each holds.

See IntervalTable.category_legend(). The column is named as in composite().

group_categories(domain, groups, new_column=None, other=None)[source]

Lumps the values of a categorical column into fewer categories.

See IntervalTable.group_categories(). The column is named as in composite().

Returns:

drillholes (DrillholeData) – A new object, with the table holding the column replaced.

fill_unlogged(domain, label, ends=True)[source]

Gives the ground a log does not account for a category of its own.

Where only the intervals of interest are logged – the mineralised ones, say – everything else is left implicit, and a model has no way of knowing it was called anything. This makes the statement explicit, so the unlogged ground can be modelled as the waste it is, in the two ways it can be missing:

  1. Intervals that exist but carry no value, which take label.

  2. Depths no interval covers, which become new intervals carrying label and nothing else. With ends this reaches from the collar to the bottom of the hole, so a hole with no intervals at all comes out logged from end to end.

The result covers each hole continuously, so validate() finds no gaps in it. The table changes support, though, so the object has to be composited again before as_point_data().

Parameters:
  • domain (str or tuple) – The categorical column, as in composite().

  • label (str) – The category to give the unlogged ground. Required: it asserts something about the rock that nobody wrote down.

  • ends (bool) – Whether to fill from the collar down to the first interval and from the last one to the bottom of the hole. The bottom is the LENGTH in the collar table, so holes with no recorded length keep whatever runs past their last interval.

Returns:

drillholes (DrillholeData) – A new object, with the table holding the column replaced.

as_point_data(tables=None, position=0.5, compositional=None, vector=None, drop_missing=True, contacts=False)[source]

Converts the interval data to points at the centre of each interval.

Every requested table must be on the same support, which compositing produces; the tables are then merged row for row. Numeric columns become continuous variables and categorical ones become categorical variables, unless they are claimed by a compositional or vector group.

The hole each point came from, its depth down that hole and the length it stands for are carried as the metadata columns HOLEID, DEPTH and LENGTH, which the models never see: they are what a leave-one-hole-out split, a contact profile and a support-weighted statistic read. Columns with the recovery role ride beside them, for the same reason – they describe the sample, not the ground.

Parameters:
  • tables (str or list) – Which interval tables to convert. All of them by default.

  • position (float) – Where in each interval the point sits, from 0 (top) to 1 (bottom). The default puts it at the centre.

  • compositional (dict) – Groups of columns to convert to compositional variables, as {name: {"columns": ["Pb", "Ag"], "rest": True}} where the table declares the units, or as {name: {"columns": {"Pb_pct": "%", "Ag_ppm": "ppm"}, "rest": True}} where it does not; a bare list of columns is the same without a rest. The units are what lets the parts be added up – see UNITS, or give a number to divide by – and each part is kept in its own unit, becoming a fraction only where the model reads it. A row missing any part is marked missing entirely; non-positive parts are replaced by half the smallest positive value of their column, since a log-ratio transform cannot take a zero; and with “rest” a further part is added holding whatever is left of the whole. Without it the parts are closed instead, which turns them into shares of what was measured.

  • vector (dict) – Groups of columns to convert to vector variables, as {name: [columns]}.

  • drop_missing (bool) – Whether to drop points whose values are all missing, which happens where a composite falls in a gap.

  • contacts (bool) – Whether to add the contacts. Every categorical column then becomes a rock type variable, an interval’s point carrying its class on both sides, and a point is added at every depth where some class changes between touching intervals of a hole – one point per depth, carrying every categorical column’s class above and below it (the same on both sides where a column runs through unchanged, empty where it has no interval), no continuous value, a LENGTH of zero and the boundary as its DEPTH. A change to or from an unlogged stretch is no contact: an empty interval, or a gap, says nothing about where the class ends.

Returns:

point (geoml.data.PointData)

get_contacts(domain)[source]

The points where the category changes, as a rock type variable.

Each contact carries the category above it and the category below it, which is what an implicit model needs to place a boundary. The hole it came from and its depth down it are carried as the metadata columns HOLEID and DEPTH; a contact has no length, so none is recorded.

An interval logged without a category is a run of its own here, so a class meeting one makes a contact with an empty side; as_point_data(contacts=True) makes none there, an unlogged stretch saying nothing about where a class ends.

Parameters:

domain (str or tuple) – The categorical column, as in composite().

Returns:

point (geoml.data.PointData)

Return type:

PointData

as_classification_input(domain, length=5.0, label_order=None)[source]

Converts a categorical column to point data for a classification model.

Unlike the compositing done for grades, this treats the category as known at a point rather than over an interval: points are dropped along each run of constant category, and the contacts between runs are added as points carrying both neighbouring categories. A boundary therefore enters the model with zero effective support, which is what lets an implicit model honour it exactly.

The hole and the length are carried as the metadata columns HOLEID and LENGTH, the latter being zero at the contacts.

Parameters:
  • domain (str or tuple) – The categorical column, as in composite().

  • length (float) – Spacing of the points dropped along each run.

  • label_order (list) – The order of the categories, for an ordered rock type. The order found in the data by default.

Returns:

point (geoml.data.PointData)

Return type:

PointData

as_pyvista(table=None)[source]

The interval data as a pyvista object, one line per interval.

Parameters:

table (str) – Which interval table to export. The first one by default.

draw_categorical(domain, colors, **kwargs)[source]

Draws the merged runs of a categorical column with plotly.

class geoml.data.drillhole.IntervalTable(data, hole='HOLEID', fr='FROM', to='TO', grades=None, categorical=None, density=None, recovery=None, flags=None, ignore=None, units=None, name=None)[source]

Bases: object

One interval file, such as an assay or a lithology table.

The table holds depths only – coordinates come from the hole traces in the DrillholeData object that owns it, so re-compositing never accumulates positional error.

Each value column is given a role, which decides how it is composited:

grade

Numeric. Length-weighted mean, weighted also by the density column if one is declared. Missing values are skipped rather than propagated.

categorical

Rock type, alteration, and the like. The category holding the greatest length within a composite wins.

density

Numeric, and also the weight applied to the grades. Composited as a length-weighted mean of its own.

recovery

Numeric: the share of each interval actually recovered. Composited as a length-weighted mean, like the density – a fraction of a length is exactly what length-weighting averages – and never applied as a weight to anything: how much a poorly recovered assay should count is a modelling decision, not a compositing one. Conversion carries it as metadata rather than as a variable, beside HOLEID and LENGTH: it describes the sample, and the models must not see it.

flag

A categorical marker (drilling method, say) treated like a category.

ignore

Carried through untouched: a composite takes the value of its single longest contributing interval. Free-text columns land here.

Columns that are not declared explicitly are given the grade role if they are numeric and ignore otherwise; set_role() corrects them afterwards. __str__ prints the roles in force.

A column may also declare what it is measured in – units={"Pb": "%", "Ag": "ppm"} – which travels with it through compositing and onto the variable the conversion builds. It is a label on an ordinary grade, and the divisor on a part of a composition, where parts in different units have to be brought to a common whole before they can be added up.

data

The intervals, sorted by hole and depth, with the hole, from and to columns renamed to HOLEID, FROM and TO.

Type:

DataFrame

roles

Column name to role.

Type:

dict

units

Column name to unit, for the columns that declare one.

Type:

dict

name

A label for the table, used in messages.

Type:

str

__init__(data, hole='HOLEID', fr='FROM', to='TO', grades=None, categorical=None, density=None, recovery=None, flags=None, ignore=None, units=None, name=None)[source]

Initializer for IntervalTable.

Parameters:
  • data (DataFrame) – The interval file.

  • hole (str) – Column with the hole name.

  • fr (str) – Columns with the start and end depths of each interval.

  • to (str) – Columns with the start and end depths of each interval.

  • grades (str or list) – Columns to assign to each role.

  • categorical (str or list) – Columns to assign to each role.

  • density (str or list) – Columns to assign to each role.

  • recovery (str or list) – Columns to assign to each role.

  • flags (str or list) – Columns to assign to each role.

  • ignore (str or list) – Columns to assign to each role.

  • units (dict) – Column name to unit – "%", "ppm", "g/t", or a number to divide by. Columns not named here are undeclared.

  • name (str) – A label for the table.

property value_columns
property holes
property length
property density_column
columns_with_role(role)[source]
Return type:

list[str]

set_role(column, role)[source]

Declares (or redeclares) how a column is to be composited.

Return type:

IntervalTable

set_unit(column, unit)[source]

Declares what a column is measured in.

A label on an ordinary grade, and the divisor on a part of a composition, where parts in different units have to be brought to a common whole before they can be added up. Anything UNITS names, or a number to divide by; None takes the declaration back.

Return type:

IntervalTable

rename(columns)[source]

Renames value columns, carrying their roles and units with them.

The roles are held beside the data, keyed by column name, so renaming the data frame’s columns directly leaves them behind: the column quietly stops being a grade, and printing or compositing the table raises. Rename here instead — or rename the data frame before it is added, which is the other place where the two cannot drift apart.

Column names become variable names in as_point_data(), so this is the last chance to tidy them before they reach a model.

Parameters:

columns (dict) – Maps each current column name to its new one.

Returns:

self, so that calls can be chained, as with set_role.

Return type:

IntervalTable

validate(on_error='warn', collar=None)[source]

Checks the intervals for the problems that break compositing.

Overlapping intervals, non-positive lengths, missing depths and holes with no collar are errors: the aggregation assumes intervals within a hole are sorted and disjoint. Gaps and intervals running past the end of the hole are only reported, since assay tables are legitimately incomplete, and so is a value above the whole its declared unit measures – 120 in a column called a percentage – which is the shape a mislabelled unit takes.

Parameters:
  • on_error (str) – One of “warn”, “raise” or “ignore”.

  • collar (DataFrame) – Collar table indexed by hole, used to check hole names and lengths.

Returns:

report (DataFrame) – One row per problem found, with its hole, depths and severity.

subset_holes(holes)[source]

A copy of this table containing the given holes only.

Return type:

IntervalTable

category_legend(column)[source]

The distinct values of a column, and how much of the hole each holds.

This is the starting point for grouping categories without a GUI: write it out with to_csv(), edit the group column in a spreadsheet – it starts as a copy of the label – and read it back into group_categories(). The rows are sorted by length, so the codes that carry the deposit come first and a long legend can be worked down from the top.

Parameters:
  • column (str) – The column to describe.

  • own (Intervals carrying no value get a row of their)

  • missing (with a)

  • label

  • the (so the legend accounts for every interval in the table and)

  • says (unlogged ground is visible while the grouping is being decided. It)

  • -- (nothing about the depths no interval covers at all)

  • intervals (DrillholeData.fill_unlogged() turns those into)

  • and

  • them. (validate() reports)

Returns:

legend (DataFrame) – One row per value, with the number of intervals it appears in, the total length it accounts for, and the group column to edit.

Return type:

DataFrame

group_categories(column, groups, new_column=None, other=None)[source]

Lumps the values of a column into fewer categories.

Logging codes are usually far too many to model directly, and which of them behave alike is a judgement only the geologist can make.

Parameters:
  • column (str) – The column to group.

  • groups (dict or DataFrame) – Either a mapping from each new category to the values it takes in, as {"ore": ["MSST", "SLTST"], "waste": "SHALE"}, or a legend from category_legend() with its group column edited.

  • new_column (str) – Where to put the result. The column is replaced by default; naming a new one keeps the original codes alongside the groups.

  • other (str) – What to call the values that no group claims. They keep their own label by default, with a warning naming them. Missing values stay missing either way.

Returns:

table (IntervalTable) – A new table; this one is left alone.

aggregate_onto(targets)[source]

Composites this table onto the given target intervals.

Parameters:

targets (DataFrame) – Intervals with the HOLEID, FROM and TO columns, sorted by hole and depth.

Returns:

table (IntervalTable) – A new table on the target support, with the same column roles.