BRO GeoTOP#

GeoST provides support for working with models provided by the BRO, such as GeoTOP. This section explains how GeoST facilitates working with GeoTOP, including performing analyses, making selections, and combining information from the model with, for example, borehole data contained in a Collection.

GeoTOP data can be read from a local NetCDF file, downloaded from the BRO, or accessed directly from the OPeNDAP server of the TNO Geological Survey. See the Reading model data section of this user guide for the available functions for reading GeoTOP data.

Working with GeoTOP#

For this example, we will use the example GeoTOP data for Utrecht Science Park:

import geost

geotop = geost.data.geotop_usp()
geotop
Downloading file 'geotop_usp.nc' from 'https://github.com/Deltares-research/geost/raw/main/data/geotop_usp.nc' to '/home/runner/.cache/geost'.
<xarray.Dataset> Size: 2MB
Dimensions:      (y: 16, x: 20, z: 121)
Coordinates:
  * y            (y) float64 128B 4.548e+05 4.548e+05 ... 4.562e+05 4.562e+05
  * x            (x) float64 160B 1.396e+05 1.396e+05 ... 1.414e+05 1.414e+05
  * z            (z) float32 484B -49.75 -49.25 -48.75 ... 9.25 9.75 10.25
    spatial_ref  int64 8B 0
Data variables: (12/13)
    strat        (y, x, z) float32 155kB ...
    lithok       (y, x, z) float32 155kB ...
    kans_1       (y, x, z) float32 155kB ...
    kans_2       (y, x, z) float32 155kB ...
    kans_3       (y, x, z) float32 155kB ...
    kans_4       (y, x, z) float32 155kB ...
    ...           ...
    kans_6       (y, x, z) float32 155kB ...
    kans_7       (y, x, z) float32 155kB ...
    kans_8       (y, x, z) float32 155kB ...
    kans_9       (y, x, z) float32 155kB ...
    onz_lk       (y, x, z) float32 155kB ...
    onz_ls       (y, x, z) float32 155kB ...
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

As you can see, GeoTOP is an xarray.Dataset containing several data variables. As explained in the Model data section, all GeoST functionality is available through the .gst accessor. For example, we can quickly inspect the bounding box and the minimum and maximum depth extents of the model.

print(geotop.gst.bounds())
print(geotop.gst.vertical_bounds())
(139500.0, 454700.0, 141500.0, 456300.0)
(-50.0, 10.5)

The most important geological information in the model is contained in the strat and lithok variables. These represent the lithostratigraphic units and their corresponding lithologies, respectively. Let’s take a look at the type of data contained in the strat and lithok variables.

print(geotop["strat"].gst.unique())
print(geotop["lithok"].gst.unique())
[1000. 2010. 3030. 3100. 4010. 5000. 5060. 5070. 5120. 6100. 6200.]
[0. 1. 2. 3. 5. 6. 7. 8.]

Inspecting GeoTOP using metadata#

We can see that the strat variable contains numeric codes for each stratigraphic unit. This makes sense from a data storage perspective, but the codes are not very intuitive when we want to know which units they represent. GeoST provides an option to quickly load the metadata supplied by the TNO Geological Survey alongside the model data for both variables. This allows us to work with the model in a more intuitive way. Let’s load both the stratigraphic and lithologic metadata. For now, we will inspect the stratigraphic metadata.

strat_units = geost.bro.geotop_strat_units()
lithok_units = geost.bro.geotop_lithok_units()

print(f"Strat metadata type : {type(strat_units)}")
print(f"Lithok metadata type : {type(lithok_units)}")
strat_units
Strat metadata type : <class 'geost.bro.geotop.GeotopUnits'>
Lithok metadata type : <class 'geost.bro.geotop.GeotopUnits'>
UnitType.STRAT
STR_UNIT_CD DESCRIPTION SEQ_NR RED_DEC GREEN_DEC BLUE_DEC
VOXEL_NR
1000 NUAAOP Antropogene afzettingen, opgebrachte grond 1.0 200 200 200
1005 NUAAES Antropogene afzettingen, esdekken 2.0 110 110 110
1010 NUNIGR Formatie van Nieuwkoop, Laagpakket van Griendt... 3.0 132 88 44
1045 NUNInb Formatie van Nieuwkoop, laag van Nij Beets 4.0 180 90 20
1020 NUNASC Formatie van Naaldwijk, Laagpakket van Schoorl 5.0 255 255 160
... ... ... ... ... ... ...
6300 NUECgd Formatie van Echteld (geulafzettingen generati... 24.0 102 153 205
6320 NUNAWOgd Formatie van Naaldwijk, Laagpakket van Wormer ... 23.0 102 153 205
6400 NUECge Formatie van Echteld (geulafzettingen generati... 26.0 27 101 175
6420 NUNAWOge Formatie van Naaldwijk, Laagpakket van Wormer ... 25.0 27 101 175
0 NN Niet formeel ingedeelde afzettingen of onbekend NaN 85 85 85

92 rows × 6 columns

The resulting objects are GeotopUnits instances, which contain a DataFrame with all metadata for the stratigraphic units in GeoTOP. GeotopUnits supports selecting units by their voxel numbers, unit codes, or parts of their unit codes, as well as by their full descriptions.

We can now use the stratigraphic metadata to determine what the unique stratigraphic numbers represent. We can do this by using the select_voxel_nr method and passing it the unique numbers.

strat_units.select_voxel_nr(geotop["strat"].gst.unique())
UnitType.STRAT
STR_UNIT_CD DESCRIPTION SEQ_NR RED_DEC GREEN_DEC BLUE_DEC
VOXEL_NR
1000 NUAAOP Antropogene afzettingen, opgebrachte grond 1.0 200 200 200
2010 NUEC2 Formatie van Echteld, gedeelte onder Formatie ... 28.0 170 255 245
3030 NUBXWI-SI-KO Formatie van Boxtel, laagpakketten van Wierden... 39.0 255 255 80
3100 NUBX Formatie van Boxtel 47.0 255 235 0
4010 NUKR-BXDE Formatie van Kreftenheye en Formatie van Boxte... 49.0 176 48 96
5000 NUDR Formatie van Drente 62.0 255 127 80
5060 NUUR2 Formatie van Urk, gedeelte onder Formatie van ... 68.0 189 183 107
5070 NUST Formatie van Sterksel 69.0 205 92 92
5120 NUPZ-WA Formatie van Peize en Formatie van Waalre 74.0 255 204 0
6100 NUECgb Formatie van Echteld (geulafzettingen generati... 11.0 102 205 171
6200 NUECgc Formatie van Echteld (geulafzettingen generati... 22.0 170 196 255

We can now clearly see which stratigraphic unit corresponds to each voxel number. For convenience, the select_voxel_nr method also accepts the xarray.Dataset of the GeoTOP model or the xarray.DataArray of the stratigraphic variable directly, producing the same selection result.

units_in_data = strat_units.select_voxel_nr(geotop)
units_in_data
UnitType.STRAT
STR_UNIT_CD DESCRIPTION SEQ_NR RED_DEC GREEN_DEC BLUE_DEC
VOXEL_NR
1000 NUAAOP Antropogene afzettingen, opgebrachte grond 1.0 200 200 200
2010 NUEC2 Formatie van Echteld, gedeelte onder Formatie ... 28.0 170 255 245
3030 NUBXWI-SI-KO Formatie van Boxtel, laagpakketten van Wierden... 39.0 255 255 80
3100 NUBX Formatie van Boxtel 47.0 255 235 0
4010 NUKR-BXDE Formatie van Kreftenheye en Formatie van Boxte... 49.0 176 48 96
5000 NUDR Formatie van Drente 62.0 255 127 80
5060 NUUR2 Formatie van Urk, gedeelte onder Formatie van ... 68.0 189 183 107
5070 NUST Formatie van Sterksel 69.0 205 92 92
5120 NUPZ-WA Formatie van Peize en Formatie van Waalre 74.0 255 204 0
6100 NUECgb Formatie van Echteld (geulafzettingen generati... 11.0 102 205 171
6200 NUECgc Formatie van Echteld (geulafzettingen generati... 22.0 170 196 255

Basic plots using metadata#

Plotting is often useful for quick data inspections. Using GeotopUnits objects, we can easily plot simple 2D sections from GeoTOP model data using familiar colours, similar to how the data are displayed in the BRO viewer. This is achieved through the get_plot_colormap method. We can use the previously created units_in_data to obtain the colormap for the units present in the model data. Let’s first see what it looks like.

colors = units_in_data.get_plot_colormap()
print(colors)
{'cmap': <matplotlib.colors.ListedColormap object at 0x7b6380135490>, 'norm': <matplotlib.colors.BoundaryNorm object at 0x7b638078fe00>}

The resulting dictionary contains a key "cmap" with a Matplotlib ListedColormap object and a "norm" with a BoundaryNorm object. The ListedColormap contains the RGB colours, while the BoundaryNorm defines the discrete intervals for each colour. The "cmap" and "norm" keys are keyword arguments for most Matplotlib functions, so the dictionary can be passed directly to many standard plotting methods. Let’s plot a section at an x-coordinate of 139850, from -10 m depth up to the surface:

geotop["strat"].sel(x=139850, z=slice(-10, None)).plot.imshow(x="y", **colors)
<matplotlib.image.AxesImage at 0x7b63797401a0>
../_images/d56a3ecfe8e36397ecc4376842d80783213fdfb986db4cfe2507588f6ba3cd01.png

Note

Note that we pass the dictionary with the double asterisk (**), which unpacks its contents as keyword arguments to the plot method.

In the same way, we can do this with the lithological metadata. Here, we do not even have to select the lithologies that are present in the model data first, because they will all be included. We will plot the same section, but now for lithology.

geotop["lithok"].sel(x=139850, z=slice(-10, None)).plot.imshow(
    x="y", **lithok_units.get_plot_colormap()
)
<matplotlib.image.AxesImage at 0x7b63795e8cd0>
../_images/6c04cd82b422b3d68a795f3f28cb42c908e2720b29b242ff04fd35d06b6654ad.png

Using metadata in analysis#

Common analyses may require an understanding of the spatial distribution of a geological formation. For example, if we want to investigate the distribution of Holocene river deposits, such as the “Formatie van Echteld”, we can use the stratigraphic metadata to select this unit and then determine its thickness. First, we select the formation based on its description.

echteld_fm = strat_units.select_description_contains("Formatie van Echteld")
echteld_fm
UnitType.STRAT
STR_UNIT_CD DESCRIPTION SEQ_NR RED_DEC GREEN_DEC BLUE_DEC
VOXEL_NR
1070 NUEC1 Formatie van Echteld, gedeelte boven Formatie ... 13.0 170 255 245
2010 NUEC2 Formatie van Echteld, gedeelte onder Formatie ... 28.0 170 255 245
6000 NUECga Formatie van Echteld (geulafzettingen generati... 9.0 118 147 60
6100 NUECgb Formatie van Echteld (geulafzettingen generati... 11.0 102 205 171
6200 NUECgc Formatie van Echteld (geulafzettingen generati... 22.0 170 196 255
6300 NUECgd Formatie van Echteld (geulafzettingen generati... 24.0 102 153 205
6400 NUECge Formatie van Echteld (geulafzettingen generati... 26.0 27 101 175

We can see that the selection result contains a group of units, in this case those related to the “Formatie van Echteld” (i.e. Echteld Formation). For convenience, GeotopUnits provides methods for retrieving predefined groups of units, such as get_anthropogenic_units, get_holocene_units, and others (see the API Reference).

We can now use the selection result to derive the thickness of the formation. The result is an xarray.DataArray, which we can plot.

echteld_thickness = geotop.gst.get_thickness(echteld_fm)
echteld_thickness.plot.imshow(
    x="x", y="y", vmin=0, vmax=12
)  # Color scale between 0 and 12 meters
<matplotlib.image.AxesImage at 0x7b63794d07d0>
../_images/fca30f433bfb7157883a5c53d046e6780bd974a364ffaf771ad8fe3f32ef74de.png

Combining with Collections#

It is often useful to extract GeoTOP data at specific locations, such as borehole locations, to compare the model with borehole data. We can easily extract GeoTOP data at borehole locations using the select_points method. Let’s load a Collection containing borehole data for the Utrecht Science Park and see how this works:

boreholes = geost.data.boreholes_usp()  # Collection instance

geotop_at_locations = geotop.gst.select_points(boreholes.header)
geotop_at_locations
<xarray.Dataset> Size: 424kB
Dimensions:      (idx: 67, z: 121)
Coordinates:
  * idx          (idx) int64 536B 0 1 2 3 4 5 6 7 8 ... 59 60 61 62 63 64 65 66
    y            (idx) float64 536B 4.56e+05 4.55e+05 ... 4.56e+05 4.56e+05
    x            (idx) float64 536B 1.396e+05 1.396e+05 ... 1.41e+05 1.412e+05
  * z            (z) float32 484B -49.75 -49.25 -48.75 ... 9.25 9.75 10.25
    spatial_ref  int64 8B 0
Data variables: (12/13)
    strat        (idx, z) float32 32kB ...
    lithok       (idx, z) float32 32kB ...
    kans_1       (idx, z) float32 32kB ...
    kans_2       (idx, z) float32 32kB ...
    kans_3       (idx, z) float32 32kB ...
    kans_4       (idx, z) float32 32kB ...
    ...           ...
    kans_6       (idx, z) float32 32kB ...
    kans_7       (idx, z) float32 32kB ...
    kans_8       (idx, z) float32 32kB ...
    kans_9       (idx, z) float32 32kB ...
    onz_lk       (idx, z) float32 32kB ...
    onz_ls       (idx, z) float32 32kB ...
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

Note

Note that you also can directly get a geost.Collection by calling geotop.gst.select_points(boreholes.header, return_collection=True).

The result is an xarray.Dataset with a new idx dimension. Note that variables such as strat now have (idx, z) dimensions. The select_points method selects the voxel columns containing each borehole, using the index order of the boreholes in the Collection.

The selection result can be used to compare individual boreholes with their corresponding voxel columns or as a starting point for further analyses. GeoST also supports adding model information to the data table of a Collection. See the examples section for more details.

Ensuring the correct model version#

Since the stratigraphic and lithologic metadata are directly tied to a specific GeoTOP model version, it is important to ensure that the metadata remain aligned with the correct model version. A newer model version may add or remove units, which could lead to unreliable results if the metadata and model are not matched correctly.

We can check the model version stored in the metadata and verify that it matches the version of the current GeoTOP model using a GeotopUnits instance.

print(f"Metadata version strat: {strat_units.geotop_version}")
print(f"Strat versions match: {strat_units.check_version_matches(geotop)}")
print(f"Metadata version lithok: {lithok_units.geotop_version}")
print(f"Lithok versions match: {lithok_units.check_version_matches(geotop)}")
Metadata version strat: v01r6s1
Strat versions match: True
Metadata version lithok: v01r6s1
Lithok versions match: True

We can see that the model version for both the stratigraphic and lithologic metadata is v01r6s1, which matches the current GeoTOP model version. If the versions do not match, a warning will be raised.

For analysis methods that use a GeotopUnits instance, such as the get_thickness example above, this version check is performed automatically. A mismatch between the metadata and model versions will therefore raise a warning. We can demonstrate this by changing the model version of the GeoTOP model and then deriving the thickness of the Echteld Formation as before.

geotop.attrs["title"] = "Changed version number"

echteld_thickness = geotop.gst.get_thickness(echteld_fm)
/home/runner/work/geost/geost/geost/bro/geotop.py:226: UserWarning: GeoTOP version mismatch: metadata version is v01r6s1, model version is 'Changed version number'
  warnings.warn(

A warning is now raised indicating that the model and metadata versions do not match. When this occurs, we recommend updating GeoST, as we aim to keep the metadata aligned with the latest GeoTOP model version. An updated version of GeoST will therefore include the corresponding updated metadata.