geoml.data
The containers, the variables they hold, and the geometry they are cut
against. Everything is addressed by tree path —
container.values("assay/Zn/prediction") — rather than by attribute chain.
Point containers
The point-based containers: _SpatialData (what every container is), _PointBased, PointData, GaussianData, DirectionalData and Section3D, with the batching contract the models read.
- class geoml.data.containers.PointData(data, coordinates)[source]
Bases:
_PointBasedData represented as points in arbitrary locations.
- as_data_frame(metadata=True, include='**', simulations=False, columns='flat')[source]
Conversion of a spatial object to a data frame.
Metadata first (bare names, the way HOLEID is read back), then the coordinates, then every filled column of every variable, named by its path – assay_Zn_prediction. include chooses what comes (“**/prediction”, “assay/**”), simulations how many realizations, and columns=”multi” keeps the path as one MultiIndex level per segment instead of flattening – for staying in pandas; written to CSV it makes several header rows, which other software reads as data.
- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- spatial_k_fold(test_data, k=5, groups=None, seed=None, name='fold')[source]
Builds cross-validation folds that mimic a prediction task.
A random fold is answered by its neighbours and flatters every score; folds pushed as far from the training data as possible overshoot the other way, testing an extrapolation nobody asked for. What decides how hard a location is to predict is how far its nearest training point sits, so the folds chosen here are the ones whose held-out-to-training distances are distributed like the distances from test_data – the object the model is actually meant to predict – to this data. This is the nearest-neighbour distance matching idea of Linnenbrink et al. (2024), built on discrete groups: continuous per-sample weightings can match the distributions perfectly while the folds are spatially wrong.
The data is first gathered into small groups that are never split across folds – the samples of one drill hole stand or fall together – and the candidate partitions come from cutting a Ward dendrogram of the group centroids at every count from k clusters up to one cluster per group, each cut’s clusters dealt to the emptiest fold largest-first. The cut whose Wasserstein distance to the target distribution is smallest wins, and the result is written to a metadata column –
"fold"unless name says otherwise, which is also what models.cross_validate reads by default.- Parameters:
test_data – The spatial object the model is meant to predict – a grid, a block model, or any container with coordinates.
k (int) – Number of folds.
groups (str, optional) – Name of a metadata column whose labels must never be split across folds (a drill hole id). Without one, the data is pre-clustered into many small spatial groups.
seed (int) – Passed to sklearn.cluster.KMeans for a reproducible pre-clustering when groups is not given. The rest of the search is deterministic.
name (str) – The metadata column to write the folds to. An existing column with this name is replaced, so two calls with two names give two labellings to compare.
- Returns:
w (float) – The Wasserstein distance between the two distributions below, in coordinate units. Zero is a perfect match.
target_distances (array) – Distance from each of test_data’s locations to its nearest data point – the prediction task.
fold_distances (array) – Distance from each data point to its nearest training point when its fold is held out – the task the cross-validation poses.
- class geoml.data.containers.GaussianData(data, coordinates_mean, coordinates_variance)[source]
Bases:
PointDataPoints whose locations are uncertain, with a variance per coordinate.
- as_data_frame(metadata=True, **kwargs)[source]
Conversion of a spatial object to a data frame.
Metadata first (bare names, the way HOLEID is read back), then the coordinates, then every filled column of every variable, named by its path – assay_Zn_prediction. include chooses what comes (“**/prediction”, “assay/**”), simulations how many realizations, and columns=”multi” keeps the path as one MultiIndex level per segment instead of flattening – for staying in pandas; written to CSV it makes several header rows, which other software reads as data.
- get_batched_variance(index=None)[source]
Variance of the input locations, mirroring get_batched_coordinates.
Zero unless the object was built with an explicit variance — see GaussianData. Only the requested batch is built: deriving the zeros from the coordinates would cost O(n_data) on every batch, which dominates prediction on large objects.
- class geoml.data.containers.DirectionalData(data, coordinates, directions)[source]
Bases:
PointData
- class geoml.data.containers.Section3D(center, azimuth, dip, width, height, n_x, n_y, coordinate_labels=('X', 'Y', 'Z'))[source]
Bases:
PointData- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
Grids
The regular grids: _GriddedData, Grid1D/2D/3D, GridND and RotatedGrid3D, plus aggregate (one implementation for every kind of variable) and the from_data box-fitting shared with the block classes.
- class geoml.data.grids.Grid1D(start, n, step=None, end=None, labels=None)[source]
Bases:
_GriddedData- __init__(start, n, step=None, end=None, labels=None)[source]
Initializer for Grid1D.
- Parameters:
start (float) – Starting point for grid.
n (int) – Number of grid nodes.
step (float | None) – Spacing between grid nodes. One number: this grid has one axis.
end (float | None) – Last grid point.
labels (str | None) – The label for the coordinate.
Either step or end must be given. If both are given, end is ignored.
- class geoml.data.grids.Grid2D(start, n, step=None, end=None, labels=None)[source]
Bases:
_GriddedData- __init__(start, n, step=None, end=None, labels=None)[source]
Initializer for Grid2D.
- Parameters:
start (length 2 array, list, or tuple) – Starting point for grid.
n (length 2 array, list, or tuple of ints) – Number of grid nodes.
step (length 2 array, list, or tuple) – Spacing between grid nodes.
end (length 2 array, list, or tuple) – Last grid point.
labels (list) – The labels for the coordinates.
Either step or end must be given. If both are given, end is ignored.
- class geoml.data.grids.Grid3D(start, n, step=None, end=None, labels=None)[source]
Bases:
_GriddedData- __init__(start, n, step=None, end=None, labels=None)[source]
Initializer for Grid3D.
- Parameters:
start (length 2 array, list, or tuple) – Starting point for grid.
n (length 2 array, list, or tuple of ints) – Number of grid nodes.
step (length 2 array, list, or tuple) – Spacing between grid nodes.
end (length 2 array, list, or tuple) – Last grid point.
labels (list) – The labels for the coordinates.
Either step or end must be given. If both are given, end is ignored.
- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- assign_from_surface(surface, name, labels=('above', 'below'), uncovered=nan)[source]
As _SpatialData.assign_from_surface, reading the sheet once for each column of cells rather than once for each cell.
A grid repeats the same (x, y) at every level — _generate varies the first axis fastest, so the pair cycles with period n_x * n_y — and a sheet depends on nothing else, which makes interpolating it n_z times over the same arithmetic n_z times. The columns are generated by the same method that generates the rows, so the two cannot fall out of step. A RotatedGrid3D does not lie on an axis-aligned lattice and takes the general path.
- class geoml.data.grids.GridND(start, n, step=None, end=None, labels=None)[source]
Bases:
_GriddedDataImplicit grid in N dimensions.
- class geoml.data.grids.RotatedGrid3D(start, n, step, azimuth=0.0, dip=0.0, rake=0.0, labels=None)[source]
Bases:
Grid3D- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- classmethod from_data(data, step, margin=0.1, decimals=0)[source]
A rotated grid fitted to another object’s spread.
The rotation is fitted to the data’s own points (a drillhole’s desurveyed cloud serves where there are no point coordinates), and the angles are rounded to decimals before anything is built from them – a grid at 47.3182 degrees is nobody’s intention – so the box is measured in the rounded frame and the data stays covered. The world origin is rounded to the same decimals.
- Parameters:
data (_SpatialData) – Any spatial object with 3-dimensional coordinates, drillholes included.
step (float | ArrayLike) – The step size, one number or one per direction.
margin (float or array) – A fraction of the unrotated box’s extent; see Grid3D.from_data.
decimals (int) – Decimals for the origin and for the azimuth, dip and rake, in degrees.
Blocks
Blocks3D is the regular block model; BlockSet3D is the variable-size
one, where every block’s origin and size are whole numbers of a base cell
so that splitting keeps it tiling exactly. Design record:
Variable block sizes in geoML — analysis, measurements, plan.
Block models: the _blockdata fan-out shared by Blocks1D/2D/3D, the variable-size BlockSet3D on its integer lattice, RotatedBlockSet3D, and the sub-block geometry behind the mesh assignments and crossed_by.
- class geoml.data.blocks.Blocks1D(start, n, step=None, end=None, labels=None, discretization=None)[source]
Bases:
Grid1D- assign_from_solid(solid, name, labels=('outside', 'inside'), fraction=None)
As _SpatialData.assign_from_solid, measuring the partial blocks on request.
fraction behaves as it does in assign_from_surface: the flag in name follows the block centre, while the column named here holds the share of each block’s sub-blocks falling inside the body — the share of its volume, for the regular sub-blocks a discretization defines.
- Parameters:
solid (Surface3D) – The closed body to test against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (outside, inside).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block inside the body. Costs prod(discretization) queries per block.
- assign_from_surface(surface, name, labels=('above', 'below'), fraction=None, uncovered=nan)
As Grid3D.assign_from_surface, measuring the partial blocks on request.
The flag in name follows the block centre, as a whole-block code does everywhere else. Name a fraction column as well and the share of each block lying below the sheet is measured over the sub-blocks discretization already defines — what a tonnage near surface needs, where counting a half-buried block whole is the error.
Where the sheet reaches part of a block but not all of it, the sub-blocks past its edge count as not below. Where it does not reach the block at all — the centre included, which is what leaves the flag empty — the fraction is uncovered instead of a measurement.
- Parameters:
surface (Surface3D) – The sheet to compare against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (above, below).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block below the sheet. Costs prod(discretization) queries per block.
uncovered (float or "raise") – What the fraction column records for a block the sheet does not reach: numpy.nan by default, so it cannot pass for a block genuinely above ground, or 0.0 to count it as nothing. Pass “raise” to refuse a surface that does not cover every block.
- discretized_coordinates(index)
- get_batched_coordinates(index)
- inducing_grid(index)
- property rows_per_location
Rows the model evaluates for each location of this object.
One, except where a location fans out into several — a block with discretization. Prediction divides the batch size by this, so that prediction_batch_size counts the rows actually handed to the model rather than meaning something different for every container.
- class geoml.data.blocks.Blocks2D(start, n, step=None, end=None, labels=None, discretization=None)[source]
Bases:
Grid2D- assign_from_solid(solid, name, labels=('outside', 'inside'), fraction=None)
As _SpatialData.assign_from_solid, measuring the partial blocks on request.
fraction behaves as it does in assign_from_surface: the flag in name follows the block centre, while the column named here holds the share of each block’s sub-blocks falling inside the body — the share of its volume, for the regular sub-blocks a discretization defines.
- Parameters:
solid (Surface3D) – The closed body to test against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (outside, inside).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block inside the body. Costs prod(discretization) queries per block.
- assign_from_surface(surface, name, labels=('above', 'below'), fraction=None, uncovered=nan)
As Grid3D.assign_from_surface, measuring the partial blocks on request.
The flag in name follows the block centre, as a whole-block code does everywhere else. Name a fraction column as well and the share of each block lying below the sheet is measured over the sub-blocks discretization already defines — what a tonnage near surface needs, where counting a half-buried block whole is the error.
Where the sheet reaches part of a block but not all of it, the sub-blocks past its edge count as not below. Where it does not reach the block at all — the centre included, which is what leaves the flag empty — the fraction is uncovered instead of a measurement.
- Parameters:
surface (Surface3D) – The sheet to compare against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (above, below).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block below the sheet. Costs prod(discretization) queries per block.
uncovered (float or "raise") – What the fraction column records for a block the sheet does not reach: numpy.nan by default, so it cannot pass for a block genuinely above ground, or 0.0 to count it as nothing. Pass “raise” to refuse a surface that does not cover every block.
- discretized_coordinates(index)
- get_batched_coordinates(index)
- inducing_grid(index)
- property rows_per_location
Rows the model evaluates for each location of this object.
One, except where a location fans out into several — a block with discretization. Prediction divides the batch size by this, so that prediction_batch_size counts the rows actually handed to the model rather than meaning something different for every container.
- class geoml.data.blocks.Blocks3D(start, n, step=None, end=None, labels=None, discretization=None)[source]
Bases:
Grid3D- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- assign_from_solid(solid, name, labels=('outside', 'inside'), fraction=None)
As _SpatialData.assign_from_solid, measuring the partial blocks on request.
fraction behaves as it does in assign_from_surface: the flag in name follows the block centre, while the column named here holds the share of each block’s sub-blocks falling inside the body — the share of its volume, for the regular sub-blocks a discretization defines.
- Parameters:
solid (Surface3D) – The closed body to test against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (outside, inside).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block inside the body. Costs prod(discretization) queries per block.
- assign_from_surface(surface, name, labels=('above', 'below'), fraction=None, uncovered=nan)
As Grid3D.assign_from_surface, measuring the partial blocks on request.
The flag in name follows the block centre, as a whole-block code does everywhere else. Name a fraction column as well and the share of each block lying below the sheet is measured over the sub-blocks discretization already defines — what a tonnage near surface needs, where counting a half-buried block whole is the error.
Where the sheet reaches part of a block but not all of it, the sub-blocks past its edge count as not below. Where it does not reach the block at all — the centre included, which is what leaves the flag empty — the fraction is uncovered instead of a measurement.
- Parameters:
surface (Surface3D) – The sheet to compare against.
name (str) – Name of the metadata column holding the whole-block flag.
labels (tuple) – What to call the two sides, in the order (above, below).
fraction (str, optional) – Name of a second metadata column, to hold the share of each block below the sheet. Costs prod(discretization) queries per block.
uncovered (float or "raise") – What the fraction column records for a block the sheet does not reach: numpy.nan by default, so it cannot pass for a block genuinely above ground, or 0.0 to count it as nothing. Pass “raise” to refuse a surface that does not cover every block.
- discretized_coordinates(index)
- get_batched_coordinates(index)
- inducing_grid(index)
- property rows_per_location
Rows the model evaluates for each location of this object.
One, except where a location fans out into several — a block with discretization. Prediction divides the batch size by this, so that prediction_batch_size counts the rows actually handed to the model rather than meaning something different for every container.
- class geoml.data.blocks.BlockSet3D(start, n, step, discretization=(2, 2, 2), max_levels=3, labels=('X', 'Y', 'Z'))[source]
Bases:
PointDataBlocks of several sizes, on one integer lattice.
A block model where the interesting ground can be carried finely and the rest coarsely. On a real deposit the ground worth resolving is a small part of the volume, and a uniform model at the resolution that part needs spends almost all of its cells saying nothing: refining 5 m only where it is wanted takes a 29-million-cell model to under 700 000.
Every block’s position and size are whole numbers of a base cell, the finest the model may go, which is step / discretization ** max_levels. Working in those integers rather than in metres is what makes it exact: blocks meet without a tolerance, a block is a whole number of its own children, and regrouping conserves mass to the last digit. It also means the model can say which of its answers rest on coarse blocks, which is the one thing a mixed-support model has to be able to prove – see docs/variable-block-models.md.
It is built full: the blocks tile their box exactly, and every operation keeps them that way. Ground to leave out is filtered, not removed, so that grouping is always safe – a half-populated group would average over blocks that are not there and quietly weigh the answer wrong.
discretization does two jobs, and they are the same job. It is how finely a block is sampled to average it, and it is how a block splits: each sub-block becomes a child. So the refinement ratio is the discretization, per axis and not necessarily two – [2, 2, 1] refines in plan and leaves the bench height alone. Being the same at every level is what lets a block of any size fan out into the same number of rows, so the model sees one shape whatever it is looking at and nothing downstream has to know that levels exist. It costs a coarse block some accuracy in its own average, always by overstating how variable it is, which errs towards splitting it – and splitting is what removes the error.
- Parameters:
start (array-like) – Centre of the first (coarsest) block.
n (array-like) – Number of blocks along each axis, at the coarsest level.
step (array-like) – Size of a coarsest-level block.
discretization (array-like) – Sub-blocks per axis, used at every level, and so also the ratio a block splits by. An axis given 1 is never refined.
max_levels (int) – How many times a block may be split. Fixes the base cell, and so the lattice everything else is counted in.
labels (list) – Coordinate names.
- block_size
(n_data, 3), each block’s size in the coordinates’ own units. Not called step_size: that name means one size for the whole object, and anything reading it would take the product of this array for a volume.
- Type:
array
- block_volume
(n_data,), what each block is worth in a tonnage.
- Type:
array
- level
(n_data,), 0 for a coarsest block up to max_levels for a base one.
- Type:
array
- property block_size
- property block_volume
- property level
- property rows_per_location
Rows the model evaluates for each location of this object.
One, except where a location fans out into several — a block with discretization. Prediction divides the batch size by this, so that prediction_batch_size counts the rows actually handed to the model rather than meaning something different for every container.
- is_full()[source]
Whether the blocks tile their box exactly – no gap, no overlap.
Volume alone cannot answer: a gap and an overlap of the same size cancel. So the base cells are counted, in an array the shape of the lattice, which costs a byte per base cell (29 MB for a 30-million-cell model). Construction is full and both split and group preserve it, so this is for checking something that came from elsewhere rather than for routine use.
- Return type:
bool
- split(mask, carry=True, labels=('X', 'Y', 'Z'))[source]
A new block set with each marked block cut into its own sub-blocks.
A block becomes prod(discretization) children, one per sub-block and in the same order, so the values a coarse prediction already holds for those sub-blocks describe the blocks this makes.
A block that was not split keeps what was predicted for it: it is the same block on the same support, so its value is still the right answer and arriving at it again would be work for nothing. The children start missing, and unpredicted() says which they are, so predict(…, where=…) visits only them. A parent’s value is never handed down – that would manufacture children agreeing exactly, which is the one thing refining is meant to find out rather than assume.
- Parameters:
mask (array-like) – One boolean per block, or the indices of the blocks to split.
carry (bool) – Whether to bring the variables and metadata across. False gives bare geometry, for building a mesh to predict onto from scratch.
- Return type:
- group(mask, carry=True, labels=('X', 'Y', 'Z'))[source]
The inverse of split: whole families of children, back into the parent they came from.
A block is grouped with its siblings, so the mask must name every child of a parent or none of them. A partial family would average over children that are not there and mis-weight the parent – the mass-conservation error the lattice exists to prevent – so it is refused rather than approximated. That check is what makes conversion between supports two-directional: group undoes split exactly, and the blocks tile their box afterwards as they did before.
A block that was not grouped keeps what it holds, exactly as in split and for the same reason: it is the same block on the same support, so its value is still the right answer for it. The parents are the ones on new ground, and they come back missing – unpredicted() names them and predict(…, where=…) fills them.
A parent’s value is never averaged from its children. Coarsening is a change of support and almost nothing survives it: a parent’s spread is not its children’s, its within-block dispersion is larger by exactly what the grouping absorbed, and a category has no mean. The one thing that would come across exactly is a realization, and a variable is more than its realizations. Metadata does come across, from the first child – it describes the ground rather than the model, and where the children disagree about it there is no right answer to be had.
- Parameters:
mask (array-like) – One boolean per block, or the indices of the blocks to group.
carry (bool) – Whether to bring across what the blocks that were not grouped hold. False gives bare geometry.
labels (list) – Coordinate names.
- Return type:
How often each decision cuts a block in two.
A continuous variable contributes one column per cut-off it declares, a categorical one per category, and both mean the same thing: the share of realizations in which this block’s sub-blocks fall on both sides of something that matters. A grade is judged against the grades someone declared; an indicator against zero, that being where one category stops winning and its rival starts.
Returns a dict of name -> (n_data,) array, empty where nothing declared a decision to make.
- Return type:
dict[str, ndarray]
- needs_splitting(split_on=None, tolerance=0.05)[source]
Which blocks hold more than one answer, and so are worth cutting.
A block whose sub-blocks agree, realization by realization, holds one answer however finely it is cut. One whose sub-blocks disagree holds two, and cutting is what separates them.
Note what this does not mark: a block the model is merely unsure about. Realizations either side of a cut-off are the model not knowing, and no amount of cutting will settle that – the answer to it is another drillhole. Only disagreement within a realization counts.
The test is over every decision at once and any one is enough, which is the cautious way round on purpose: a block worth splitting for one variable is worth splitting whatever the others say. Name split_on to narrow it – letting every element of a polymetallic deposit vote marks most of the model and gives back the saving.
Blocks already at the finest level are never marked, there being nothing to cut them into.
- Parameters:
split_on (str or list, optional) – Which variables get a say. All of them by default.
tolerance (float) – The share of realizations that must find the block divided. Small but not zero, so that one realization in twenty does not carry it.
- Return type:
ndarray
- unbalanced(gap=1)[source]
Blocks with a neighbour more than gap levels finer than they are.
A block whose own sub-blocks agree is never marked by needs_splitting, and rightly so – cutting it would not change the answer it gives. But the field can still turn sharply inside it, and nothing in the block itself says so. That a neighbour was cut twice while this block was not cut at all is the evidence, and it lives outside the block.
It matters for what is drawn rather than for what is decided. A contour reads a block through its eight corners, so a coarse block beside much finer ones is a crude straight guess across a long span laid right where the surface runs. Levelling the jump measured three times closer to the true surface for 35% more blocks, where refining a whole level deeper without it bought almost nothing for 2.6 times as many – deeper refinement widens the jumps as fast as it narrows the blocks. models.refine therefore cuts these as it goes.
Blocks already at the finest level are never marked, there being nothing to cut them into.
- Parameters:
gap (int) – How many levels of difference to tolerate. One is the usual 2:1 balance: a block may meet blocks one level finer, not two.
- Return type:
ndarray
- classmethod from_data(data, step, margin=0.1, decimals=0, discretization=(2, 2, 2), max_levels=3)[source]
A block model covering another object’s bounding box.
As Grid3D.from_data, counting blocks rather than nodes: the margined box’s lower corner is floored to decimals, and enough blocks follow to cover the far side, so the corner is round and the margin never shrinks.
- Parameters:
data – Any spatial object, drillholes included.
step – The coarse block size, one number or one per direction.
margin (float or array) – A fraction of the data’s extent; see Grid3D.from_data.
decimals (int) – Decimals to floor the box corner to.
discretization – As the constructor takes them.
max_levels – As the constructor takes them.
- index_data(data)[source]
Which block each of data’s locations falls in.
One row index per location, -1 for anything outside the box. Note this is not what a grid’s index_data returns – a cell index per axis – because blocks of several sizes have no per-axis index to return. Which block is the answer here.
The lattice makes it cheap: a location’s base cell is arithmetic, and the block covering that cell is the one whose origin is the cell’s ancestor at that block’s own level, so one searchsorted per level finds it and every location is settled within max_levels + 1 of them.
- Return type:
ndarray
- aggregate(data, variables=None, metadata=True)[source]
Carries another object’s measurements onto the blocks holding them.
As Grid3D.aggregate – one method, the operation following each variable’s kind – over blocks of several sizes.
- assign_from_surface(surface, name, labels=('above', 'below'), fraction=None, uncovered=nan)[source]
As Blocks3D.assign_from_surface, over blocks of several sizes.
The flag in name follows the block centre; naming a fraction column measures the share of each block below the sheet over the sub-blocks discretization defines, scaled to each block’s own size. crossed_by asks the same question and answers with the blocks worth cutting.
- assign_from_solid(solid, name, labels=('outside', 'inside'), fraction=None)[source]
As Blocks3D.assign_from_solid, over blocks of several sizes.
fraction holds the share of each block’s volume inside the body, and crossed_by turns the same test into the blocks worth cutting.
- crossed_by(mesh)[source]
Which blocks a mesh passes through, and so which are worth cutting.
A block is crossed when its sub-blocks fall on both sides of the mesh – the question needs_splitting asks of a cut-off, asked of geometry instead. A topography, a vein wall, a lease boundary: a block the surface runs through holds two answers whatever is predicted into it, and no amount of prediction will separate them.
One entirely above or entirely below is left alone however close it lies, which is what keeps this from refining a whole domain. Blocks already at the finest level are never marked, as elsewhere.
Hand it to split, or give the mesh to models.refine, which unions this with the other two criteria and repeats until nothing is left to cut:
blocks = blocks.split(blocks.crossed_by(topography))
A sheet that covers only part of the model counts a sub-block past its edge as not below, the way fraction does, so a block straddling the sheet’s own boundary reads as crossed. That is usually wanted – the edge is a real feature of the ground being described – but it is why a sheet should reach across the model when it is not.
- unpredicted(variable=None)[source]
Which blocks have nothing in them yet.
What split leaves behind: hand it to predict(…, where=…) and only the blocks the refinement created are visited. Without a variable it is the blocks the last split made, which is what a container knows without having to be told what was predicted into it; naming one reads its missing values instead, which stays true however the object was arrived at.
- Return type:
ndarray
- as_data_frame(metadata=True, **kwargs)[source]
Conversion of a spatial object to a data frame.
Metadata first (bare names, the way HOLEID is read back), then the coordinates, then every filled column of every variable, named by its path – assay_Zn_prediction. include chooses what comes (“**/prediction”, “assay/**”), simulations how many realizations, and columns=”multi” keeps the path as one MultiIndex level per segment instead of flattening – for staying in pandas; written to CSV it makes several header rows, which other software reads as data.
- Return type:
DataFrame
- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
One hexahedron per block, written out corner by corner, rather than the ImageData a regular block model exports: implicit geometry can only say one spacing, and the point here is that there is more than one. The cells are welded, so blocks that touch share the corners they meet at – which is what lets anything be contoured across them.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- get_contour(path, value, supersample=1, simplify=None, close=False)[source]
Isosurface through blocks of more than one size.
marching_cubes wants a rectangular array and there is none to give it, so the cells are handed to VTK instead, which contours an unstructured grid directly. On a model of one block size the two agree exactly; here the answer is the one a regular grid could not have produced without carrying every block at the finest size.
Values live on the cells and an isosurface needs them on the corners, so they are averaged onto the corners first – the blocks meeting at a corner are what decide where the surface passes.
The blocks the surface runs through are cut to the finest size the lattice allows before any of that, in the mesh handed to VTK and not in the model: a coarse block cannot see the corners its finer neighbours place in the middle of the face they share, so the two sides draw different curves there and the surface tears along every such interface it crosses. Cutting its neighbourhood to one size puts the interfaces out of the way. Nothing is predicted – a child reads its parent’s value and the shape its corners carry – and the surface comes back the one a model carried at the finest size throughout would have given. See _cut_to_contour.
- Parameters:
path (str) – The column to contour, named the way the tree names it: “grade/prediction” is that column, “grade” alone defaults to the variable’s prediction, and “Elements/Zn” reaches a component (only the components of a composition hold a grade). A single bare segment that is no variable of its own is searched for anywhere in the tree, so “Zn” still finds “Elements/Zn” as long as only one variable holds a Zn.
value (float) – The value to draw the surface at.
supersample (int) – How many levels past the model’s own finest block to cut the mesh to. Costs prod(discretization) times the cells per level, around the surface only, and buys a rounder and closer surface rather than merely a prettier one – what VTK reads between block corners is trilinear, and creasing at every face is what looks blocky. One level is worth roughly predicting a model several times the size; past that it flattens off. Zero to leave the mesh at the model’s own resolution.
simplify (float, optional) – A geometric error budget, in coordinate units: the surface is simplified until pushing further would move it more than this (see Mesh3D.simplify). Pairs naturally with supersample, which buys accuracy in triangles this then spends back where the surface is flat. None returns the full triangulation.
close (bool or str) – Whether to close the surface where it runs out of the model, so that what comes back is a body rather than a sheet with a hole in the side – “above” (or True) keeps the region where the values exceed value, a grade shell, and “below” the region under it, as on a grid. Done with a shell of ghost cells mirroring the boundary blocks, each its partner’s own size, so the closing cap cannot tear whatever the refinement did to the boundary.
- Returns:
surf (Solid3D, Surface3D or Mesh3D) – Whichever the geometry calls for, as get_contour on a grid.
- class geoml.data.blocks.RotatedBlockSet3D(start, n, step, azimuth=0.0, dip=0.0, rake=0.0, discretization=(2, 2, 2), max_levels=3, labels=('X', 'Y', 'Z'))[source]
Bases:
BlockSet3DA variable-size block model rotated about its starting block.
The lattice is BlockSet3D’s, untouched: splitting, grouping, the refinement criteria and the integer arithmetic all happen in the unrotated frame, which is what keeps them exact. The rotation is applied where coordinates leave – the block centres, the sub-block fan-out a prediction reads, the exported hexahedra – and removed where coordinates come in (index_data, and so aggregate). Every mesh test and assignment reads sub-block positions through get_batched_coordinates, so geometry against surfaces and solids works in world coordinates with nothing overridden.
- index_data(data)[source]
Which block each of data’s locations falls in.
One row index per location, -1 for anything outside the box. Note this is not what a grid’s index_data returns – a cell index per axis – because blocks of several sizes have no per-axis index to return. Which block is the answer here.
The lattice makes it cheap: a location’s base cell is arithmetic, and the block covering that cell is the one whose origin is the cell’s ancestor at that block’s own level, so one searchsorted per level finds it and every location is settled within max_levels + 1 of them.
- classmethod from_data(data, step, margin=0.1, decimals=0, discretization=(2, 2, 2), max_levels=3)[source]
A rotated block model fitted to another object’s spread.
As RotatedGrid3D.from_data – the rotation fitted to the points and rounded to decimals (degrees) before the box is measured – counting blocks rather than nodes.
Meshes
Triangulated meshes: Mesh3D the primitive, Surface3D and Solid3D as siblings, DTM3D the terrain, mesh3d picking by geometry, the booleans, the DXF round trip, and the adapters every container’s assignments read (_sheet_interpolator, _closed_body, _side_codes). The arithmetic itself is geoml.math.geometry; what lives here is what touches a container or holds an error message.
- class geoml.data.meshes.Mesh3D(points, triangles, normals)[source]
Bases:
_PointBasedA triangulated surface: vertices, the triangles indexing them, normals.
The primitive Surface3D and Solid3D are built on, and the only one of the three that promises nothing about its shape — which is what a mesh must be allowed to be while it is still being repaired. What it does do is measure itself as it is built, so that everything downstream can ask rather than work it out again: area, and whether it is closed and consistent. Those cost a few milliseconds on a mesh of tens of thousands of triangles.
mesh3d(points, triangles, normals) builds whichever of the three the geometry calls for, and is what the readers use.
- area
The surface area, whether or not the mesh closes.
- Type:
float
- closed
Whether every edge is shared by two triangles, so that the mesh bounds a volume. Vacuously true of an empty mesh.
- Type:
bool
- consistent
Whether the triangles agree about which way is out. A closed mesh that is not consistent bounds nothing that can be tested.
- Type:
bool
- split()[source]
The mesh’s connected pieces, each as an object of its own.
A boolean operation readily answers with a body in several pieces — an ore shell cut in two by a fault — and each piece is a body in its own right, while together they are still one legitimate mesh. This is how to take them apart; each piece comes back as whichever class its own geometry calls for.
- Returns:
pieces (list) – One mesh per connected piece, longest-standing order. A mesh already in one piece returns [self].
- heal(hole_size=None)[source]
A repaired copy of this mesh.
Three things are put right, in the order that works: coincident vertices are welded, so that seams stop reading as boundaries; holes smaller than hole_size are covered over; and the triangles are made to agree about which way is out, then turned to face outward. That last step is not optional — filling a hole leaves the new triangles wound however they came, which would leave the mesh closed and still untestable.
What comes back is whichever class the repaired geometry calls for, which may be the same one, and may be an empty Mesh3D if nothing survived. Healing is not guaranteed: a mesh with a hole larger than hole_size, or one self-intersecting, can come back no better.
- Parameters:
hole_size (float, optional) – The largest hole to cover, in the mesh’s own units. None to weld and reorient only, leaving every boundary where it is.
- Returns:
mesh (Mesh3D, Surface3D or Solid3D)
- simplify(max_error)[source]
The same shape on as few triangles as the error budget allows.
Built for what get_contour returns: a contoured surface carries a triangle for every block corner it crosses, most of them slivers saying nothing the budget would miss. The argument is geometric – how far, in the mesh’s own units, the simplified surface may sit from the original – so the same call means the same thing on a coarse shell and a fine one, which a fraction of triangles does not.
The caller’s kind is kept: a body stays a body, a terrain a terrain. If the reduction breaks the kind’s own promise – a solid opened, a terrain folded over – the constructor refuses as it always does; allow less error and try again.
- Parameters:
max_error (float) – The largest distance the simplified surface may sit from the original, in the mesh’s own units. Enforced by measurement: the simplified faces are probed against the original surface, and the decimation tightened until the promise holds.
- Returns:
mesh (the same class as this one.)
- smooth(iterations=20, pass_band=0.1)[source]
A smoothed copy, by Taubin’s non-shrinking filter.
Cosmetic, and priced honestly: applied to a block-model contour this was measured to take away a sixth of the faceting while moving the surface 50% further from the true level set – the creases go, and accuracy goes with them, which is why no contour smooths itself. For a surface that is both rounder and closer to the truth, contour with supersample instead; smooth when the look of the mesh is what matters.
The caller’s kind is kept, as in simplify.
- Parameters:
iterations (int) – Passes of the filter; more is smoother.
pass_band (float) – The filter’s pass band, in (0, 2): lower smooths more.
- Returns:
mesh (the same class as this one.)
- classmethod from_dxf(filename)[source]
Reads a triangulated surface from a DXF file.
Three ways of writing a triangulation are understood. The MESH entity that export_dxf writes already holds a vertex list and the faces that index into it, and is taken as it stands. POLYFACE meshes and loose 3DFACE entities instead repeat the coordinates of every corner they share, and are welded back into shared vertices, matched to six decimal places. Faces with more than three corners are split into a fan of triangles.
Every mesh in the file is read and the results are concatenated, so a file holding several bodies comes back as one surface in several disconnected pieces. Each MESH entity keeps its own vertices, while the welded entities share one vertex list, so pieces that meet there are joined. Entities nested inside blocks are not searched.
Only the geometry is read: see export_dxf on what a DXF file has no room for.
- Parameters:
filename (str) – Path of the file to read.
- Returns:
mesh (Surface3D, Solid3D or Mesh3D) – Whichever the geometry read calls for, with normals computed from the triangles (see geometry.vertex_normals), since a DXF file carries none.
- export_dxf(filename, offset=None)[source]
Writes this surface to a DXF file, as a single MESH entity.
A MESH holds the vertex list and the triangles that index into it, so the surface comes back from from_dxf exactly as it went out – nothing is welded and there is no ceiling on the number of vertices, unlike the POLYFACE mesh DXF is more often written as.
Only the geometry travels. A DXF file has nowhere to put the variables and metadata a surface carries: to_zarr keeps a container whole, and as_pyvista carries the values onto a mesh object.
- Parameters:
filename (str) – Path of the file to write.
offset (array-like) – Added to the coordinates on the way out, as in export_micromine, for writing into a local grid. It is not recorded in the file, so reading it back gives the shifted coordinates.
- export_micromine(points_filename='points', triangles_filename='triangles', offset=[0, 0, 0], **kwargs)[source]
- as_pyvista(simulations=False, include='**')[source]
Converts this object to a pyvista one, carrying its variables.
- Parameters:
simulations – Which simulations to include: False for none (the default, since each one is a full-length array in the exported object), True for all of them, an int for the first n, or a sequence of indices.
- class geoml.data.meshes.Surface3D(points, triangles, normals)[source]
Bases:
Mesh3DA mesh that does not close: a sheet, with an edge to it.
A topography, a seam roof, a weathering front, a fault plane — anything that has two sides rather than an inside. The promise is checked where it is made, so assign_from_surface need only be given one of these.
- intersection(other)[source]
The part of this sheet lying inside a body, or under a terrain.
Against a body, the sheet is cut where it crosses the body’s surface, so what comes back follows the body’s shape rather than the triangles’ — the piece of a fault plane inside an ore envelope, say. Against a single-valued sheet — a topography — the cut is against the ground below it, keeping what lies under. A sheet lying wholly outside comes back empty.
- difference(other)[source]
The part of this sheet lying outside a body, or over a terrain.
The complement of intersection: together the two hold the whole sheet. A sheet lying wholly inside comes back empty.
- clip_meshes(meshes)[source]
Everything below this sheet, each mesh cut to its own kind.
The batch form of cutting against a terrain: one ground body is extruded under the sheet and serves every cut, where cutting one by one would rebuild it per mesh. A body comes back a closed body (the boolean engines see to it), a sheet comes back a sheet — a shell that runs out of its model is open, and stays open here; contour it with close= first if a body is what is wanted.
- Parameters:
meshes (sequence of Mesh3D) – Bodies and sheets to cut below this one.
- Returns:
list – One cut mesh per input, in the same order.
- class geoml.data.meshes.Solid3D(points, triangles, normals)[source]
Bases:
Mesh3DA mesh that closes: a body, with an inside.
An ore envelope, a stope, a dyke, a contoured shell. Both promises are checked where they are made — the mesh must close, and its triangles must agree which way is out — so assign_from_solid need only be given one of these, and volume always means something.
A body wound inwards is turned round on the way in rather than refused: nothing about it is ambiguous, only reversed. The triangles are what get reversed; the normals are left as they were given.
- volume
The volume enclosed, always positive. Zero for an empty body, which is what an intersection of two bodies that do not meet comes to.
- Type:
float
- union(other)[source]
A body covering everything either of these two covers.
Two bodies that do not meet make a union in two pieces, which is one legitimate body; split() takes it apart.
- intersection(other)[source]
A body covering what both of these two cover, empty where they do not meet at all.
- difference(other)[source]
A body covering what this one covers and the other does not.
Where other lies wholly inside this one the answer is this body with a cavity in it, which is written as both surfaces, the inner one turned inwards — so volume comes to the difference of the two, and a location in the cavity tests as outside.
- class geoml.data.meshes.DTM3D(points, triangles, normals)[source]
Bases:
Surface3DA terrain: a sheet standing at one height over each (x, y).
A digital terrain model, and the shape most of the surfaces in a project have — a topography, a seam roof, a weathering front. The promise is that it never folds back over itself, checked where the object is made, which is what lets a body be divided into what lies under it and what lies over it, and what makes “the elevation here” a question with one answer.
Not what mesh3d returns: an ordinary sheet is a Surface3D unless a terrain is asked for, this being a promise to make rather than a fact to detect. Triangles standing exactly vertical are allowed, a cliff being single valued everywhere but along the line of its face.
- geoml.data.meshes.mesh3d(points, triangles, normals)[source]
A mesh of whichever class its geometry calls for.
A Solid3D where the triangles close and agree which way is out, a Surface3D where they do not close, and a plain Mesh3D where they close but disagree — the one case that is neither a sheet nor a body, and what Mesh3D.heal exists for.
- Parameters:
points (array) – An (n, 3) array of vertex coordinates.
triangles (array) – An (m, 3) array of vertex indices.
normals (array) – An (n, 3) array of vertex normals.
- Returns:
mesh (Surface3D, Solid3D or Mesh3D)
Variables
What a container holds at each location: measurements, and everything a model writes back.
The variable family: _Variable and the concrete kinds a container holds (continuous, vector, compositional, categorical, binary), each declaring its own columns for the tree machinery in base to fold over. Constructed by a container’s add_*_variable methods, never directly.
- class geoml.data.variables.ContinuousVariable(name, coordinates, measurements=None)[source]
Bases:
_VariableRepresentation of a continuous random variable.
- measurements
The raw measurements.
- Type:
_Attribute
- latent_mean
The mean of the latent Gaussian representation.
- Type:
_Attribute
- latent_variance
The variance of the latent Gaussian representation.
- Type:
_Attribute
- dispersion
How much the locations inside each block differ among themselves – the variance over a block’s sub-blocks, averaged over the realizations, in the variable’s own units rather than the latent ones. A different question from latent_variance, which is how sure the model is of the block: a well-known block can still be heterogeneous, and that is what decides whether cutting it finer would tell anyone anything. Filled only where the container discretizes; elsewhere a location has no interior and this stays missing rather than zero.
- Type:
_Attribute
- noise_variance
How far a fresh measurement here would fall from the value above – the likelihood noise carried into the variable’s own units, averaged over the realizations. The third of three variances and the third question: latent_variance is how sure the model is of the value, dispersion is how much the ground varies inside a block, and this is how much a sample of it would scatter. A prediction reports the ground, with the noise integrated out, so this is what has to be added back to compare against an assay. Missing where the prediction was made with include_noise=False, there being no integration to read it from.
- Type:
_Attribute
- simulations
Draws from the variable’s posterior distribution, in a single (n_data, n_sim) array. Use simulation() to get one of them as an _Attribute.
- Type:
- quantiles
The variables quantiles, indexed by the corresponding percentile.
- Type:
dict
- probabilities
Cumulative distribution probabilities, indexed by the corresponding quantile.
- Type:
dict
- responsibilities
Under a Mixture likelihood, how likely each measurement is to have come from each of its noise components, indexed by the component’s position. Empty otherwise; written by set_responsibilities.
- Type:
dict
- measurements: _Attribute
- latent_mean: _Attribute
- latent_variance: _Attribute
- prediction: _Attribute
- dispersion: _Attribute
- noise_variance: _Attribute
- quantiles: dict[float, _Attribute]
- probabilities: dict[float, _Attribute]
- cutoffs: list[float] | None
- proportions: dict[float, _Attribute]
- divided: dict[float, _Attribute]
- responsibilities: dict[int, _Attribute]
- set_cutoffs(cutoffs)[source]
The grades this variable is judged against.
They travel with the variable, so a model trained on data that declares them hands them to every block model predicted from it, and refine knows what the blocks have to be resolved against without being told a second time.
- Return type:
- reset_quantiles(probabilities=None)[source]
Resets the variable’s quantiles.
- Parameters:
probabilities (ArrayLike | None) – Probabilities between 0 and 1, exclusive, at which to take the quantiles.
- reset_probabilities(quantiles=None)[source]
Resets the variable’s probabilities.
- Parameters:
quantiles (ArrayLike | None) – Values in the variable’s own units, at which to take the cumulative probabilities.
- compute_metrics(alpha=0.05)[source]
Scores this variable’s prediction against its own measurements.
- Parameters:
alpha – Significance level for the interval-based scores.
- Returns:
dict – One entry per score, named.
Notes
The spread-based scores here – goodness, coverage, CRPS, the interval score – are of the ground: a container’s simulations have the likelihood’s noise integrated out, so they describe a quantity no sample observes, while the measurements they are compared against carry it. Those scores therefore read pessimistic on held-out data, by the share of the variance the model calls noise, and increasingly so for a model with more capacity, which calls less of it noise. Measured on Jura, a nominal 90% band read 0.59 here against 0.94 through the measurement distribution.
For calibration on data the model has not seen, use
geoml.models.cross_validate(), whose scores come fromgeoml.models.VGPNetwork.predict_measurements(), or the accuracy figure, which asks the model for the same thing. The location-wise scores (rmse, mae, bias) are unaffected: integrating the noise out changes the spread, not the value.See also
geoml.models.VGPNetwork.predict_measurementsthe distribution an assay is drawn from.
geoml.models.cross_validateout-of-fold scores, of measurements.
- class geoml.data.variables.DerivedVariable(name, coordinates, parents=None)[source]
Bases:
ContinuousVariableA variable computed from others, realization by realization.
The middle ground between metadata (a constant the models never see) and a modelled variable (measured, likelihooded, written by a model): it carries a full set of simulations and everything built on them – quantiles, cut-offs, contours, grade-tonnage – but every bit of its uncertainty is inherited from the variables it was derived from. Built by derive on the container, never fed to a model. Applying the function to each realization and summarizing afterwards is what keeps a nonlinear function honest: f(E[grades]) is not E[f(grades)], and the second is the answer.
The recipe – the function itself – lives in the script that ran derive, not here: functions do not survive a Zarr store honestly. A reloaded container has the values, fully usable; re-deriving is running the script again. parents records which paths it came from.
- class geoml.data.variables.VectorVariable(name, coordinates, labels, measurements=None)[source]
Bases:
_Variable- components: dict[str, ContinuousVariable]
- uncertainty: _Attribute
- responsibilities: dict[int, _Attribute]
- prediction_input()[source]
The components’ cut-offs, as one row each.
They are declared per component – two grades are judged against two different numbers – but the model sees the variable whole, so they travel as a matrix with a row per component. A component declaring fewer than the widest is padded with infinity, which nothing is ever above, so its spare columns come back empty and update drops them.
- class geoml.data.variables.CompositionalVariable(name, coordinates, labels, measurements=None)[source]
Bases:
VectorVariable
- class geoml.data.variables.RockTypeVariable(name, coordinates, labels=None, measurements_a=None, measurements_b=None)[source]
Bases:
_Variable- components: dict[str, _Category]
- predicted: _Attribute
- entropy: _Attribute
- uncertainty: _Attribute
- measurements_a: _Attribute
- measurements_b: _Attribute
- boundary: _Attribute
- class geoml.data.variables.CategoricalVariable(name, coordinates, labels=None, measurements=None)[source]
Bases:
RockTypeVariable
- class geoml.data.variables.OrderedRockType(name, coordinates, labels=None, measurements_a=None, measurements_b=None)[source]
Bases:
RockTypeVariable
- class geoml.data.variables.BinaryVariable(name, coordinates, labels=None, measurements=None)[source]
Bases:
_Variable- indicator: _Attribute
- measurements: _Attribute
- weights: _Attribute
- predicted: _Attribute
- probability: _Attribute
- entropy: _Attribute
- uncertainty: _Attribute
- latent_mean: _Attribute
- latent_variance: _Attribute
Paths
How a variable’s columns are named and addressed. Design record: Naming, reaching and exporting a variable’s parts — analysis and plan.
The container tree: the errors, the bounding box, the path grammar (VariablePath, render), the traversal (_TreeNode) and the leaf it carries (_Attribute). Everything a container or a variable is built on, and nothing that is one.
- exception geoml.data.base.NoDataError[source]
Bases:
ExceptionException raised when a data object is empty.
- exception geoml.data.base.NotGriddedDataError[source]
Bases:
ExceptionException raised when expecting a gridded data object.
- exception geoml.data.base.NotClosedError[source]
Bases:
ValueErrorA mesh that does not bound a volume was asked to.
- exception geoml.data.base.InconsistentMeshError[source]
Bases:
ValueErrorA closed mesh whose triangles disagree about which way is out.
- exception geoml.data.base.NotSingleValuedError[source]
Bases:
ValueErrorA sheet that folds over was asked which of its heights to use.
- exception geoml.data.base.MeshTypeError[source]
Bases:
ValueErrorTwo meshes were combined in a way that means nothing.
- exception geoml.data.base.DimensionMismatchError[source]
Bases:
ExceptionException raised when the dimensionality of objects does not match.
- class geoml.data.base.BoundingBox(min_values, max_values)[source]
Bases:
objectAn n-dimensional box.
- __init__(min_values, max_values)[source]
An n-dimensional box.
- Parameters:
min_values (array) – The box’s minimum values in each direction.
max_values (array) – The box’s maximum values in each direction.
- property n_dim
- property diagonal
- property min
- property max
- property center
- overlaps_with(other)[source]
Checks if box overlaps with another box.
- Parameters:
other (BoundingBox) – The other box.
- Returns:
check (bool) – The checking result.
- class geoml.data.base.VariablePath(parts=())[source]
Bases:
objectWhere a piece of data sits inside a container.
A container holds variables, a variable holds components or attributes, and an attribute holds one array per location. This names a place in that tree the way a file system names a file –
assay/Zn/noise_variance– so that one string can serve the lookup, the persistence key and the exported column name.Built from a string, from parts, or from another path; / composes, as it does for pathlib:
>>> VariablePath("assay") / "Zn" / "prediction" VariablePath('assay/Zn/prediction')
- parts
- property name
The last segment, or ‘’ for the root.
- property parent
- geoml.data.base.render(path, style='path')[source]
An addressable path as a name in some flat namespace.
Purely mechanical – the segments joined, nothing else. That is the point: the four spellings this replaced each had rules of their own about which role was abbreviated and which was dropped, and no two agreed.
path is what the store and every internal caller use, flat is for data-frame columns, CSV and mining software (identifier-safe), and pretty is for pyvista and ParaView, where the name is read by a person.
- geoml.data.base.render_all(paths, style='flat')[source]
{path: name} for a whole namespace at once, every name distinct.
A path cannot collide – / is refused inside a segment – so a collision is made by the join, and only flat makes one readily: _ is in nearly every role name, so a variable noise with a component variance lands on the same column as a leaf called noise_variance.
The rule has to be deterministic or an export changes shape between runs, so a colliding group is sorted by path and the ones after the first take a suffix. Only the group is touched: suffixing all of them would penalize the innocent column to spare the pathological one. Adding a variable can therefore rename a column inside a colliding group, which is why this warns rather than quietly putting it right.