Skip to content

Add a borehole grid: vertical profiles at a set of sites - #5

Open
lispandfound wants to merge 1 commit into
synthetic-datafrom
borehole-grid
Open

lispandfound wants to merge 1 commit into
synthetic-datafrom
borehole-grid

Conversation

@lispandfound

@lispandfound lispandfound commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Comparing a velocity model against a borehole log, or handing a 1-D profile to
a site-response code, only needs one column of samples per station. The
existing grids all fill a rotated box, so getting there has meant generating a
whole volume and slicing it, or hand-rolling a Grid outside the config system
entirely (which the README talked people through).

grid.type = "borehole" extracts one column per site, running from the
topography down to a fixed depth at a fixed vertical resolution, so profiles
from different sites line up sample for sample. The grid is shaped
(len(sites), 1, nk): the singleton j axis keeps the (i, j, k) contract
every layer relies on, so the whole layer chain — coastline, offshore, Ely,
clamp, query — works unchanged.

No model origin

A borehole grid is a bag of independent columns, so there is nothing for a
model origin to be the origin of. Model therefore splits: Projection holds
the CRS and its cached transformers, and Model extends it with the origin
and azimuth the extent-based grids lay themselves out against. A borehole
config takes [grid.projection] and no origin at all; the Grid attributes
that record one are derived from the sites.

Longitude and latitude place a site; nothing else is reserved

Sites come from an inline list, or from a CSV or Parquet file, in a global CRS
(WGS84 by default, sites_crs to override).

Every other key in a [[grid.sites]] block, and every other column of a site
file, becomes a coordinate on the i axis under the name the caller gave it,
so a station code, a network or a driller's reference reaches the output:

[[grid.sites]]
longitude = 172.15
latitude = -43.70
site = "GULL"          # not a keyword
network = "NZ"

A label keeps the type it was written with, so a numeric column arrives
numeric. Mashumaro folds the extra keys in a __pre_deserialize__ hook, so
the longitude and latitude validators still run and a config file needn't
spell the labels mapping out. keep_extra_columns = false drops the labels
and keeps only the spatial coordinates.

Two things that has to guard.

A label can't shadow a grid name. A coordinate shadows a variable or
attribute of the same name, so a label called name would turn grid.name
from the grid's name into a DataArray and break {grid.name: grid} in the
builder. I confirmed that before relying on it:

>>> grid.name
'boreholes'
>>> grid.assign_coords(name=("i", ["A", "B"])).name
<xarray.DataArray 'name' (i: 2)>          # not the grid's name any more

RESERVED_COORDINATES in nzcvm.grids.grid is derived from GridSchema's
own fields, plus the logical index, the coastline coordinate and the
components that share that index, so it keeps up if the schema changes.

Sites have to agree on their labels. A missing key is nearly always a
typo, and the alternative is a column of nulls. keep_extra_columns = false
skips both checks, since there is nothing left to collide or disagree.

Testing

just synthetic
uv run nzcvm generate examples/borehole.toml synthetic/boreholes.zarr
uv run --dev pytest tests/          # 240 passed, 1 skipped
uv run --dev pytest --doctest-modules nzcvm/
uv run ruff check nzcvm/ tests/ && uv run ty check && uv run deptry .

Vs down each 600 m column of examples/borehole.toml, sampled every 100 m:

Site Where Vs (m/s)
GULL inside the gully basin 500, 500, 680, 680, 980, 980, 980
TERR inside the terrace basin 500, 680, 680, 980, 980, 1515, 1526
RIDG hills, no basin 340, 1123, 1340, 1463, 1536, 1565, 1577
SEAB offshore 380, 580, 580, 580, 580, 580, 580

GULL stays in the basin's 1-D profile the whole way down; TERR breaks out of
its shallower basin into the tomography at 500 m; RIDG starts on the Ely taper
and drops straight into tomography; SEAB follows the offshore profile.

tests/test_borehole.py covers the Projection/Model split, site loading
from inline lists, CSV and Parquet, the labels (inline keys, file columns, a
numeric label, the opt-out, and both error paths), the grid geometry against
the analytic topography, and a round trip through map_blocks and the Zarr
writer to confirm the labels survive.
🤖 Generated with Claude Code


Stack created with GitHub Stacks CLIGive Feedback 💬

@lispandfound
lispandfound added this pull request to stack #6 September 10, 2026 06:56
@github-actions

Copy link
Copy Markdown

Benchmark for b144bfc

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 235.7±3.68ns 237.3±3.05ns +0.68%
Mesh_Point_Query/far_corner/4096000 226.0±1.74ns 232.9±2.66ns +3.05%
Mesh_Point_Query/near_origin/4096000 224.7±3.86ns 227.5±3.21ns +1.25%

@github-actions

Copy link
Copy Markdown

Benchmark for 60572e7

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 215.8±6.60ns 221.3±6.87ns +2.55%
Mesh_Point_Query/far_corner/4096000 158.5±6.61ns 165.3±5.29ns +4.29%
Mesh_Point_Query/near_origin/4096000 213.5±7.45ns 217.0±8.82ns +1.64%

@github-actions

Copy link
Copy Markdown

Benchmark for bd31a03

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 292.6±5.24ns 316.3±1.50ns +8.10%
Mesh_Point_Query/far_corner/4096000 201.5±5.62ns 222.1±6.83ns +10.22%
Mesh_Point_Query/near_origin/4096000 348.0±2.07ns 349.5±1.95ns +0.43%

Comparing a velocity model against a borehole log, or handing a 1-D profile
to a site-response code, only needs one column of samples per station. The
existing grids all fill a rotated box, so getting there has meant generating
a whole volume and slicing it, or hand-rolling a Grid outside the config
system entirely (which the README talked people through).

`grid.type = "borehole"` extracts one column per site, running from the
topography down to a fixed depth at a fixed vertical resolution, so profiles
from different sites line up sample for sample. The grid is shaped
`(len(sites), 1, nk)`: the singleton `j` axis keeps the `(i, j, k)` contract
every layer relies on, so the whole layer chain — coastline, offshore, Ely,
clamp, query — works unchanged.

Sites come from an inline list, or from a CSV or Parquet file. They are
given in a global CRS (WGS84 by default, `sites_crs` to override) and mapped
into the grid CRS.

Longitude and latitude place a site and the config reserves nothing else.
Every other key in a `[[grid.sites]]` block, and every other column of a
site file, becomes a coordinate on the `i` axis under the name the caller
gave it, so a station code, a network or a driller's reference reaches the
output. A label keeps the type it was written with. Mashumaro folds the
extra keys in a `__pre_deserialize__` hook, so the longitude and latitude
validators still run and a config file needn't spell the mapping out.
`keep_extra_columns = false` drops the labels and keeps only the spatial
coordinates.

Two things that has to guard. A coordinate shadows a variable or attribute
of the same name, so a label called `name` would turn `grid.name` from the
grid's name into a DataArray. `RESERVED_COORDINATES` is derived from
`GridSchema`'s own fields, plus the logical index, the coastline coordinate
and the components that share that index, so it keeps up if the schema
changes. Sites also have to agree on which labels they carry, since a
missing key is nearly always a typo and the alternative is a column of
nulls.

A borehole grid is a bag of independent columns, so there is nothing for a
model origin to be the origin of. `Model` therefore splits: `Projection`
holds the CRS and its cached transformers, and `Model` extends it with the
origin and azimuth the extent-based grids lay themselves out against. A
borehole config takes `[grid.projection]` and no origin at all; the Grid
attributes that record one are derived from the sites.

`examples/borehole.toml` runs four profiles over the `just synthetic`
dataset, two of them inside a basin, and the tests build their columns on
the synthetic topography so the expected elevations follow from closed form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Benchmark for d405d59

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 315.2±1.29ns 315.7±3.37ns +0.16%
Mesh_Point_Query/far_corner/4096000 299.2±5.86ns 298.6±2.25ns -0.20%
Mesh_Point_Query/near_origin/4096000 366.8±3.47ns 367.9±5.70ns +0.30%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant