3D viewing and VTK export#

GeoST makes use of PyVista to allow the user to export data to Visualization Toolkit (VTK) format as well as interactively view data in 3D. In this example we create a simple 3D plot of some boreholes and the GeoTOP model in the area of the Utrecht Science Park (USP).

Let’s import GeoST and PyVista and load the data we are going to work with:

import pyvista as pv

import geost

# Load the boreholes from the Utrecht Science Park (USP) area
boreholes_usp = geost.data.boreholes_usp()

# Load the GeoTOP model from the OpenDAP server based on the bounding box of the boreholes
# Load = True because the area is small, so we prefer loading over streaming data
geotop_usp = geost.read_geotop_from_opendap(
    bbox=boreholes_usp.header.total_bounds, load=True
)

# Load REGIS from the OpenDAP server based on the bounding box of the boreholes
regis_usp = geost.read_regis_from_opendap(
    bbox=boreholes_usp.header.total_bounds, load=True
)
Downloading file 'boreholes_usp.parquet' from 'https://github.com/Deltares-research/geost/raw/main/data/boreholes_usp.parquet' to '/home/runner/.cache/geost'.
print("Boreholes:\n")
boreholes_usp
Boreholes:
Collection
  header (rows, columns) : (67, 6)
  data (rows, columns)   : (1398, 32)
crs: RD
vertical datum: NAP height
print("Voxel model:\n")
geotop_usp
Voxel model:
<xarray.Dataset> Size: 4MB
Dimensions:      (y: 13, x: 18, z: 313)
Coordinates:
  * y            (y) float64 104B 4.548e+05 4.548e+05 ... 4.558e+05 4.56e+05
  * x            (x) float64 144B 1.396e+05 1.396e+05 ... 1.412e+05 1.412e+05
  * z            (z) float32 1kB -49.75 -49.25 -48.75 ... 105.2 105.8 106.2
    spatial_ref  int64 8B 0
Data variables: (12/13)
    strat        (y, x, z) float32 293kB 5.07e+03 5.07e+03 5.07e+03 ... nan nan
    lithok       (y, x, z) float32 293kB 3.0 3.0 3.0 3.0 3.0 ... nan nan nan nan
    kans_1       (y, x, z) float32 293kB 0.0 0.0 0.0 0.0 0.0 ... nan nan nan nan
    kans_2       (y, x, z) float32 293kB 3.0 2.0 7.0 9.0 8.0 ... nan nan nan nan
    kans_3       (y, x, z) float32 293kB 27.0 38.0 53.0 61.0 ... nan nan nan nan
    kans_4       (y, x, z) float32 293kB 0.0 0.0 0.0 0.0 0.0 ... nan nan nan nan
    ...           ...
    kans_6       (y, x, z) float32 293kB 42.0 40.0 26.0 22.0 ... nan nan nan nan
    kans_7       (y, x, z) float32 293kB 8.0 6.0 7.0 6.0 7.0 ... nan nan nan nan
    kans_8       (y, x, z) float32 293kB 0.0 0.0 0.0 0.0 0.0 ... nan nan nan nan
    kans_9       (y, x, z) float32 293kB 0.0 0.0 0.0 0.0 0.0 ... nan nan nan nan
    onz_lk       (y, x, z) float32 293kB 69.0 65.0 64.0 56.0 ... nan nan nan nan
    onz_ls       (y, x, z) float32 293kB 0.0 0.0 0.0 0.0 0.0 ... nan nan nan nan
Attributes:
    title:         geotop v01r6s1 (100.0m * 100.0m * 0.5m
    references:    http://www.dinoloket.nl/detaillering-van-de-bovenste-lagen...
    comment:       GeoTOP 1.6.1 (lithoklasse)
    disclaimer:    http://www.dinoloket.nl
    terms_of_use:  http://www.dinoloket.nl
    institution:   TNO / Geologische Dienst Nederland
    Conventions:   CF-1.4
    source:        Generated by nl.tno.dino.geo3dmodel.generator.GeoTOPVoxelM...
    history:       Generated on Thu May 01 22:54:36 CEST 2025
print("Layer model:\n")
regis_usp
Layer model:
<xarray.Dataset> Size: 1MB
Dimensions:  (layer: 131, y: 13, x: 18)
Coordinates:
  * layer    (layer) <U9 5kB 'NUhlc' 'NUBXz1' 'NUBXSCk1' ... 'CKVAc' 'CKAKc'
  * y        (y) float64 104B 4.548e+05 4.548e+05 ... 4.558e+05 4.56e+05
  * x        (x) float64 144B 1.396e+05 1.396e+05 ... 1.412e+05 1.412e+05
    crs      int64 8B 0
Data variables:
    top      (layer, y, x) float32 123kB 2.09 1.91 1.57 1.17 ... nan nan nan nan
    bottom   (layer, y, x) float32 123kB -2.95 -2.85 -2.71 -2.35 ... nan nan nan
    hgv      (layer, y, x) float32 123kB 9.969e+36 9.969e+36 ... 9.969e+36
    kD       (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
    c        (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
    kh       (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
    kv       (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
    sdh      (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
    sdv      (layer, y, x) float32 123kB nan nan nan nan nan ... nan nan nan nan
Attributes: (12/39)
    references:                    https://www.dinoloket.nl/regis-ii-het-hydr...
    Conventions:                   CF-1.7
    creator_url:                   https://www.dinoloket.nl
    keywords_vocabulary:           NASA/GCMD Earth Science Keywords. Version 6.0
    acknowledgment:                https://www.dinoloket.nl
    project:                       REGIS v02r2s3
    ...                            ...
    geospatial_lon_units:          degrees_east
    geospatial_lon_resolution:     0.0014598784
    geospatial_vertical_min:       -1235.92
    geospatial_vertical_max:       322.75
    geospatial_vertical_units:     m-NAP
    geospatial_vertical_positive:  up

We have a Collection containing 67 boreholes, an xarray.Dataset of the GeoTOP voxel model and an xarray.Dataset representing the REGIS layer model. GeoST offers a few options to turn these into PyVista objects:

Object

Method

PyVista object

Description

Collection

to_pyvista_cylinders

PyVista.MultiBlock

Fancy cylinder geometries of boreholes

Collection

to_pyvista_grid

PyVista.UnstructuredGrid

Simple geometries, useful for large datasets

xarray Dataset or DataArray (Voxel model)

gst.to_pyvista_grid(structured=True)

PyVista.ImageData

Structured grid, useful for large datasets

xarray Dataset or DataArray (Voxel model)

gst.to_pyvista_grid(structured=False)

PyVista.UnstructuredGrid

Individual rendering of each voxel

xarray Dataset or DataArray (Layer model)

gst.to_pyvista_grid

PyVista.UnstructuredGrid

Individual rendering of each voxel

We will apply to_pyvista_grid for our boreholes to get a PyVista.UnstructuredGrid and the accessor method to_pyvista_grid with structured=True (default) for the GeoTOP model to get a PyVista.ImageData object. The loaded REGIS layer model will be converted to a PyVista.UnstructuredGrid.

Note

If your xarray Dataset or DataArray represents a layer model, the ‘structured’ argument will be ignored as a layer model can only be converted to an unstructured grid

# The boreholes are converted to an unstructured grid of the lithology data. The borehole
# geometries are given a radius of 10 meters.
boreholes_usp_pyvista = boreholes_usp.to_pyvista_grid("lith", radius=10)

# The GeoTOP model is converted to an structured grid of the stratigraphy data
geotop_usp_pyvista = geotop_usp.gst.to_pyvista_grid("strat")

# The REGIS model is converted to an unstructured grid. We first slice it between -80
# and -120 m NAP for better visual separation with GeoTOP later on.
regis_usp = regis_usp.gst.slice_depth_interval(upper=-80, lower=-120)
regis_usp_pyvista = regis_usp.gst.to_pyvista_grid(["kh", "kv"])

# Let's see what the objects look like
print("Boreholes PyVista grid:\n", boreholes_usp_pyvista)
print("\n")
print("GeoTOP PyVista grid:\n", geotop_usp_pyvista)
print("\n")
print("REGIS PyVista grid:\n", regis_usp_pyvista)
Boreholes PyVista grid:
 UnstructuredGrid (0x7a2a58e6cee0)
  N Cells:    1398
  N Points:   11184
  X Bounds:   1.395e+05, 1.413e+05
  Y Bounds:   4.548e+05, 4.560e+05
  Z Bounds:   -2.712e+02, 8.037e+00
  N Arrays:   1


GeoTOP PyVista grid:
 ImageData (0x7a2a58e6f6a0)
  N Cells:      73242
  N Points:     83524
  X Bounds:     1.396e+05, 1.414e+05
  Y Bounds:     4.548e+05, 4.560e+05
  Z Bounds:     -4.975e+01, 1.068e+02
  Dimensions:   19, 14, 314
  Spacing:      1.000e+02, 1.000e+02, 5.000e-01
  N Arrays:     1


REGIS PyVista grid:
 UnstructuredGrid (0x7a2a58e42ce0)
  N Cells:    491
  N Points:   1604
  X Bounds:   1.395e+05, 1.413e+05
  Y Bounds:   4.547e+05, 4.560e+05
  Z Bounds:   -1.200e+02, -8.000e+01
  N Arrays:   2

You can use the save method of these PyVista objects to export them as VTK files and load them into your viewer of choice.

Note

Tip: if an UnstructuredGrid is large (several millions of points and faces) run the clean method with a tolerance of e.g. 1e-5, which merges points that are close to each other. This decreases file size and RAM usage during rendering. Also save the model as vtu instead of vtk file, which is a compressed vtk-format to further save disk-space.

We typically export the PyVista objects to VTK and use ParaView to view data in 3D.

# Example of saving the PyVista objects to files
# boreholes_usp_pyvista.save("boreholes.vtu")
# geotop_usp_pyvista.save("geotop.vtk")
# regis_usp_pyvista.save("regis.vtu")

Paraview visualization example:

GeoTOP and IC model