Convert common Python geospatial data structures to and from the geoh5 format used by ANALYST and other Mira Geoscience tools.
geoh5-bridge wraps geoh5py and
provides a simple, Pythonic API for common conversion tasks — including
bidirectional bridges to PyVista and the
Open Mining Format (OMF).
| Source | Target | Function |
|---|---|---|
xarray.DataArray (raster / GeoTIFF via rioxarray) |
Grid2D |
raster_to_grid2d() |
xarray.DataArray (raster) |
Points |
raster_to_points() |
Grid2D |
xarray.DataArray |
grid2d_to_raster() |
xarray.Dataset / DataArray (3-D) |
BlockModel |
xarray_to_blockmodel() |
BlockModel |
xarray.Dataset |
blockmodel_to_xarray() |
| Source | Target | Function |
|---|---|---|
geopandas.GeoDataFrame (Point geometries) |
Points |
geodataframe_to_points() |
Points |
geopandas.GeoDataFrame |
points_to_geodataframe() |
geopandas.GeoDataFrame (LineString / MultiLineString) |
Curve |
geodataframe_to_curve() |
Curve |
geopandas.GeoDataFrame |
curve_to_geodataframe() |
geopandas.GeoDataFrame (Polygon / MultiPolygon) |
Surface |
geodataframe_to_surface() |
Surface |
geopandas.GeoDataFrame |
surface_to_geodataframe() |
| Source | Target | Function |
|---|---|---|
Points |
pyvista.PolyData |
points_to_pyvista() |
pyvista.PolyData |
Points |
pyvista_to_points() |
Grid2D |
pyvista.StructuredGrid |
grid2d_to_pyvista() |
pyvista.StructuredGrid |
Grid2D |
pyvista_to_grid2d() |
Curve |
pyvista.PolyData (lines) |
curve_to_pyvista() |
pyvista.PolyData (lines) |
Curve |
pyvista_to_curve() |
Surface |
pyvista.PolyData (mesh) |
surface_to_pyvista() |
pyvista.PolyData (mesh) |
Surface |
pyvista_to_surface() |
BlockModel |
pyvista.RectilinearGrid |
blockmodel_to_pyvista() |
pyvista.RectilinearGrid |
BlockModel |
pyvista_to_blockmodel() |
| Source | Target | Function |
|---|---|---|
omf.PointSetElement |
pyvista.PolyData |
omf_pointset_to_pyvista() |
pyvista.PolyData |
omf.PointSetElement |
pyvista_to_omf_pointset() |
omf.LineSetElement |
pyvista.PolyData (lines) |
omf_lineset_to_pyvista() |
pyvista.PolyData (lines) |
omf.LineSetElement |
pyvista_to_omf_lineset() |
omf.SurfaceElement (SurfaceGeometry or SurfaceGridGeometry) |
pyvista.PolyData (mesh) |
omf_surface_to_pyvista() |
pyvista.PolyData (mesh) |
omf.SurfaceElement (SurfaceGeometry) |
pyvista_to_omf_surface() |
omf.VolumeElement |
pyvista.RectilinearGrid |
omf_volume_to_pyvista() |
pyvista.RectilinearGrid |
omf.VolumeElement |
pyvista_to_omf_volume() |
omf.Project |
pyvista.MultiBlock |
omf_project_to_pyvista() |
pyvista.MultiBlock |
omf.Project |
pyvista_to_omf_project() |
| Source | Target | Function |
|---|---|---|
omf.PointSetElement |
Points |
omf_pointset_to_points() |
Points |
omf.PointSetElement |
points_to_omf_pointset() |
omf.LineSetElement |
Curve |
omf_lineset_to_curve() |
Curve |
omf.LineSetElement |
curve_to_omf_lineset() |
omf.SurfaceElement (SurfaceGeometry or SurfaceGridGeometry) |
Surface or Grid2D |
omf_surface_to_surface() |
omf.SurfaceElement (SurfaceGridGeometry, uniform spacing) |
Grid2D |
omf_surface_to_grid2d() |
Surface |
omf.SurfaceElement (SurfaceGeometry) |
surface_to_omf_surface() |
Grid2D |
omf.SurfaceElement (SurfaceGridGeometry) |
grid2d_to_omf_surface() |
omf.VolumeElement |
BlockModel |
omf_volume_to_blockmodel() |
BlockModel |
omf.VolumeElement |
blockmodel_to_omf_volume() |
pip install geoh5-bridge # core (geoh5py + numpy)
pip install geoh5-bridge[raster] # + rioxarray, xarray, rasterio
pip install geoh5-bridge[vector] # + geopandas
pip install geoh5-bridge[grid3d] # + xarray, netcdf4
pip install geoh5-bridge[pyvista] # + pyvista
pip install geoh5-bridge[omf] # + omf, pyvista
pip install geoh5-bridge[all] # everything
pip install geoh5-bridge[dev] # all + pytestimport rioxarray # registers the "rasterio" engine
import xarray as xr
from geoh5py.workspace import Workspace
from geoh5_bridge import raster_to_grid2d
da = xr.open_dataarray("elevation.tif", engine="rasterio")
with Workspace.create("output.geoh5") as ws:
grid = raster_to_grid2d(da, ws, name="Elevation")Multi-band rasters are supported — each band is stored as a separate data channel on the Grid2D.
from geoh5_bridge import raster_to_points
with Workspace.create("output.geoh5") as ws:
pts = raster_to_points(da, ws, name="ElevPoints", nodata=-9999)import geopandas as gpd
from geoh5_bridge import geodataframe_to_points, points_to_geodataframe
gdf = gpd.read_file("sample_points.geojson")
with Workspace.create("output.geoh5") as ws:
pts = geodataframe_to_points(gdf, ws, name="SamplePoints")
# Round-trip back to a GeoDataFrame
gdf_out = points_to_geodataframe(pts)All numeric attribute columns are attached as data channels
automatically. Use data_columns=["col1", "col2"] to select specific
columns, or z_column="elevation" to use a column for the z-coordinate.
from geoh5_bridge import geodataframe_to_curve, curve_to_geodataframe
gdf = gpd.read_file("roads.geojson")
with Workspace.create("output.geoh5") as ws:
curve = geodataframe_to_curve(gdf, ws, name="Roads")
gdf_out = curve_to_geodataframe(curve)from geoh5_bridge import geodataframe_to_surface, surface_to_geodataframe
gdf = gpd.read_file("parcels.geojson")
with Workspace.create("output.geoh5") as ws:
surf = geodataframe_to_surface(gdf, ws, name="Parcels")
gdf_out = surface_to_geodataframe(surf)Polygon and MultiPolygon geometries are triangulated and stored as a Surface mesh. Concave polygons and polygons with holes are handled correctly.
import xarray as xr
from geoh5_bridge import xarray_to_blockmodel, blockmodel_to_xarray
ds = xr.open_dataset("model.nc")
with Workspace.create("output.geoh5") as ws:
bm = xarray_to_blockmodel(ds, ws, name="DensityModel")
# Round-trip back to xarray
ds_out = blockmodel_to_xarray(bm)Dimension names are auto-detected from common conventions (x/easting,
y/northing, z/depth, etc.) or can be specified explicitly with the
dims=("easting", "northing", "depth") parameter.
geoh5-bridge provides bidirectional conversions between all major
geoh5py object types and their PyVista equivalents. Requires
pip install geoh5-bridge[pyvista].
import pyvista as pv
from geoh5_bridge import points_to_pyvista, pyvista_to_points
with Workspace.create("output.geoh5") as ws:
# geoh5 → PyVista
cloud = points_to_pyvista(pts)
# PyVista → geoh5
pts2 = pyvista_to_points(cloud, ws, name="CloudPoints")from geoh5_bridge import grid2d_to_pyvista, pyvista_to_grid2d
with Workspace.create("output.geoh5") as ws:
sg = grid2d_to_pyvista(grid)
grid2 = pyvista_to_grid2d(sg, ws, name="Grid")from geoh5_bridge import curve_to_pyvista, pyvista_to_curve
with Workspace.create("output.geoh5") as ws:
pd_lines = curve_to_pyvista(curve)
curve2 = pyvista_to_curve(pd_lines, ws, name="Lines")from geoh5_bridge import surface_to_pyvista, pyvista_to_surface
with Workspace.create("output.geoh5") as ws:
mesh = surface_to_pyvista(surf)
surf2 = pyvista_to_surface(mesh, ws, name="Mesh")from geoh5_bridge import blockmodel_to_pyvista, pyvista_to_blockmodel
with Workspace.create("output.geoh5") as ws:
rg = blockmodel_to_pyvista(bm)
bm2 = pyvista_to_blockmodel(rg, ws, name="Model")All PyVista bridge functions accept an optional data_names list to
select which data channels to include.
Bidirectional conversions between Open Mining Format (OMF)
elements and PyVista meshes. Unlike omfvista, this bridge also supports
the reverse direction (PyVista → OMF). Requires
pip install geoh5-bridge[omf].
import omf
from geoh5_bridge import (
omf_pointset_to_pyvista, pyvista_to_omf_pointset,
omf_lineset_to_pyvista, pyvista_to_omf_lineset,
omf_surface_to_pyvista, pyvista_to_omf_surface,
omf_volume_to_pyvista, pyvista_to_omf_volume,
omf_project_to_pyvista, pyvista_to_omf_project,
)
# Load an OMF project and convert every element to PyVista
reader = omf.OMFReader("model.omf")
project = reader.get_project()
multiblock = omf_project_to_pyvista(project) # pyvista.MultiBlock
# Modify in PyVista and write back to OMF
project2 = pyvista_to_omf_project(multiblock, project_name="Updated")
omf.OMFWriter(project2, "model_updated.omf")Individual element conversions follow the same pattern:
# OMF → PyVista
pd = omf_pointset_to_pyvista(pointset)
pd = omf_lineset_to_pyvista(lineset)
pd = omf_surface_to_pyvista(surface_elem) # handles both SurfaceGeometry
# and SurfaceGridGeometry (see below)
rg = omf_volume_to_pyvista(volume)
# PyVista → OMF
pointset = pyvista_to_omf_pointset(pd, name="Samples")
lineset = pyvista_to_omf_lineset(pd, name="Drillholes")
surf_elem = pyvista_to_omf_surface(mesh, name="Fault")
volume = pyvista_to_omf_volume(rg, name="Density")OMF SurfaceElement objects can use one of two geometry representations:
SurfaceGeometry — explicit triangle mesh
The most common type. Stores a list of 3-D vertices and a list of
triangle index triples (exactly like a geoh5py Surface).
import omf
import numpy as np
# Build a simple triangle
geom = omf.SurfaceGeometry(
vertices=np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=float),
triangles=np.array([[0, 1, 2]], dtype=int),
)
elem = omf.SurfaceElement(name="Fault", geometry=geom)
# Convert to PyVista PolyData
mesh = omf_surface_to_pyvista(elem)SurfaceGridGeometry — structured (regular) grid surface
Used for DEM-like or map-view surfaces where the XY layout is a
regular grid and each node may have an independent Z elevation
(offset_w). Defined by:
| Attribute | Type | Description |
|---|---|---|
origin |
[x, y, z] |
Grid origin (corner node) |
tensor_u |
float[] |
Cell spacings along the U axis (len = number of columns) |
tensor_v |
float[] |
Cell spacings along the V axis (len = number of rows) |
axis_u |
[dx, dy, dz] |
Unit vector for the U direction |
axis_v |
[dx, dy, dz] |
Unit vector for the V direction |
offset_w |
float[] or None |
Per-node Z displacement, row-major order j*(nu+1)+i |
The node count is (len(tensor_u)+1) × (len(tensor_v)+1). Each grid
cell is split into two triangles, so omf_surface_to_pyvista() returns
a PolyData with 2 × len(tensor_u) × len(tensor_v) triangles.
import omf
import numpy as np
# 3×2 cell grid (4 nodes wide, 3 nodes tall) in the XY plane
geom = omf.SurfaceGridGeometry(
origin=np.array([0.0, 0.0, 0.0]),
tensor_u=np.array([10.0, 10.0, 10.0]), # 3 columns → 4 node columns
tensor_v=np.array([10.0, 10.0]), # 2 rows → 3 node rows
axis_u=np.array([1.0, 0.0, 0.0]),
axis_v=np.array([0.0, 1.0, 0.0]),
# Optional: per-node elevation offsets (4×3 = 12 nodes, row-major)
offset_w=np.array([0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5], dtype=float),
)
elem = omf.SurfaceElement(name="Topography", geometry=geom)
# Grid is triangulated automatically → pyvista.PolyData (12 triangles)
mesh = omf_surface_to_pyvista(elem)Tip:
pyvista_to_omf_surface()always writes back asSurfaceGeometry(explicit triangles), which is universally compatible with all OMF readers.
Convert OMF elements directly to/from geoh5py objects without an
intermediate PyVista step. Requires pip install geoh5-bridge[omf].
Both SurfaceGeometry and SurfaceGridGeometry are fully supported.
When a grid surface has uniform cell spacing and no per-node
elevation offsets (offset_w), omf_surface_to_surface() returns a
Grid2D by default (controlled by the prefer_grid2d parameter). In
all other cases — explicit triangle meshes, non-uniform spacing, or grids
with elevation offsets — a triangulated Surface is returned.
import omf
from geoh5py.workspace import Workspace
from geoh5_bridge import (
Points, Curve, Surface, BlockModel,
omf_pointset_to_points, points_to_omf_pointset,
omf_lineset_to_curve, curve_to_omf_lineset,
omf_surface_to_surface, surface_to_omf_surface,
omf_volume_to_blockmodel, blockmodel_to_omf_volume,
)
reader = omf.OMFReader("model.omf")
project = reader.get_project()
with Workspace.create("output.geoh5") as ws:
for element in project.elements:
if isinstance(element, omf.PointSetElement):
omf_pointset_to_points(element, ws)
elif isinstance(element, omf.LineSetElement):
omf_lineset_to_curve(element, ws)
elif isinstance(element, omf.SurfaceElement):
# SurfaceGridGeometry with uniform spacing → Grid2D (default)
# SurfaceGeometry or non-uniform/offset grids → Surface
omf_surface_to_surface(element, ws)
elif isinstance(element, omf.VolumeElement):
omf_volume_to_blockmodel(element, ws)To force all surfaces to be triangulated as Surface regardless of
geometry type, pass prefer_grid2d=False:
with Workspace.create("output.geoh5") as ws:
for element in project.elements:
if isinstance(element, omf.SurfaceElement):
# Always returns a Surface (triangulated)
omf_surface_to_surface(element, ws, prefer_grid2d=False)Reverse direction — export geoh5 objects to OMF:
from geoh5py.objects import Grid2D
from geoh5_bridge import grid2d_to_omf_surface
with Workspace("existing.geoh5") as ws:
for obj in ws.objects:
if isinstance(obj, BlockModel):
elem = blockmodel_to_omf_volume(obj)
elif isinstance(obj, Grid2D):
# Grid2D → SurfaceGridGeometry (preserves implicit grid structure)
elem = grid2d_to_omf_surface(obj)
elif isinstance(obj, Surface):
# Surface → SurfaceGeometry (explicit triangles)
elem = surface_to_omf_surface(obj)
elif isinstance(obj, Curve):
elem = curve_to_omf_lineset(obj)
elif isinstance(obj, Points):
elem = points_to_omf_pointset(obj)For OMF grid surfaces that map cleanly to a geoh5py Grid2D (uniform
cell spacing, no per-node elevation offsets, axes aligned to the
horizontal plane), use omf_surface_to_grid2d() directly:
import omf
import numpy as np
from geoh5py.workspace import Workspace
from geoh5_bridge import omf_surface_to_grid2d
# Build a 4×3 cell uniform grid surface
geom = omf.SurfaceGridGeometry(
origin=np.array([500000.0, 7000000.0, 350.0]),
tensor_u=np.full(4, 25.0), # 4 columns, 25 m spacing
tensor_v=np.full(3, 25.0), # 3 rows, 25 m spacing
axis_u=np.array([1.0, 0.0, 0.0]),
axis_v=np.array([0.0, 1.0, 0.0]),
# No offset_w → flat surface at z = 350 m
)
elem = omf.SurfaceElement(name="Topography", geometry=geom)
with Workspace.create("output.geoh5") as ws:
grid = omf_surface_to_grid2d(elem, ws, name="Topography")
# grid is a geoh5py Grid2D:
# origin = (500000, 7000000, 350)
# u_cell_size = 25.0, u_count = 4
# v_cell_size = 25.0, v_count = 3
# rotation = 0°, dip = 0°omf_surface_to_grid2d() raises TypeError if the element does not use
SurfaceGridGeometry, and ValueError if the spacing is non-uniform or
offset_w is present (use omf_surface_to_surface() for those cases).
grid2d_to_omf_surface() converts a geoh5py Grid2D back to an OMF
SurfaceElement with SurfaceGridGeometry, preserving the implicit grid
structure (uniform spacing, rotation, and dip):
import omf
from geoh5py.workspace import Workspace
from geoh5_bridge import grid2d_to_omf_surface
with Workspace("existing.geoh5") as ws:
grid = ws.get_entity("Topography")[0] # a Grid2D object
elem = grid2d_to_omf_surface(grid, name="Topography")
# elem.geometry is SurfaceGridGeometry — compact, no triangulation needed
project = omf.Project(name="Export", elements=[elem])
omf.OMFWriter(project, "topography.omf")The rotation angle (degrees CCW from East) maps to axis_u, and the
dip angle (degrees above horizontal) controls the tilt of axis_v. A
dip of 0° yields a flat horizontal grid; 90° yields a vertical
plane.
When the OMF surface uses SurfaceGridGeometry, the function first checks
whether the grid is compatible with Grid2D (uniform spacing, no
offset_w). If prefer_grid2d=True (the default) and the check passes,
it delegates to omf_surface_to_grid2d() and returns a Grid2D.
Otherwise it reconstructs the grid node positions from origin,
tensor_u/tensor_v, and axis_u/axis_v, applies any per-node Z
offsets from offset_w, and stores the result as a triangulated Surface.
| OMF geometry | prefer_grid2d |
offset_w |
uniform spacing | Result |
|---|---|---|---|---|
SurfaceGeometry |
any | N/A | N/A | Surface (explicit triangles) |
SurfaceGridGeometry |
True (default) |
absent | yes | Grid2D |
SurfaceGridGeometry |
True (default) |
present | any | Surface (triangulated) |
SurfaceGridGeometry |
True (default) |
absent | no | Surface (triangulated) |
SurfaceGridGeometry |
False |
any | any | Surface (triangulated) |
where nu = len(tensor_u) and nv = len(tensor_v). When triangulated,
the node count is (nu+1) × (nv+1) and the triangle count is
2 × nu × nv.
Note:
surface_to_omf_surface()always exports a geoh5pySurfaceasSurfaceGeometry(explicit triangles), preserving the mesh exactly. ForGrid2Dobjects, usegrid2d_to_omf_surface()to preserve the compact grid representation. A grid surface imported from OMF viaomf_surface_to_surface()withprefer_grid2d=Falseand then exported back to OMF will be stored as explicit triangles rather than as a grid, which is compatible with all OMF readers but uses more storage.
| Package | Description |
|---|---|
| geoh5py | Official Python API for the geoh5 file format |
| rioxarray | Rasterio xarray extension for reading/writing geospatial rasters |
| geopandas | Spatial operations on geometric types built on pandas |
| xarray | N-D labelled arrays and datasets |
| Package | Description |
|---|---|
| omf | Official Python library for reading and writing OMF files |
| omfvista | PyVista interface for OMF (OMF → PyVista only; geoh5-bridge adds the reverse) |
| Package | Description |
|---|---|
| PyVista | 3-D plotting and mesh analysis (VTK wrapper) — useful for visualising BlockModels and Surfaces |
| GeoVista | Cartographic rendering and mesh analytics powered by PyVista |
| geoh5vista | PyVista interface for geoh5 objects — read geoh5 directly into PyVista meshes |
| pyvista-xarray | xarray accessor for PyVista — plot xarray data in 3-D with PyVista |
| Package | Description |
|---|---|
| lasio | Read/write Log ASCII Standard (LAS) well-log files |
| welly | Manage well data with an easy-to-use Python interface |
| striplog | Lithology and stratigraphic logs |
| geoh5py Drillhole | geoh5py natively supports Drillhole objects for collar + survey + interval data |
| Package | Description |
|---|---|
| SimPEG | Simulation and parameter estimation for geophysics — can export to geoh5 |
| discretize | Meshing utilities for SimPEG (TensorMesh ↔ BlockModel) |
| verde | Spatial data processing and gridding |
| rasterio | Low-level raster I/O |
git clone https://github.com/RichardScottOZ/geoh5-bridge.git
cd geoh5-bridge
pip install -e ".[dev]"
pytestMIT