Kriging with GSTools

This example utilizes data available from the FORGE geothermal reserach site’s 2019 Geothermal Design Challenge. In this example, the data is archived in the Open Mining Format v1 (OMF) specification and the omfvista software is leverage to load those data into a PyVista MultiBlock data structure.

The goal of this workflow is to create a 3D temperature model by kriging the Observed Temperature data (sparse observational data). The open-source, Python software GSTools is used to perform variogram analysis and kriging of the temperature data onto a PyVista mesh to create the 3D model.

import PVGeo

# sphinx_gallery_thumbnail_number = 4
from gstools import Exponential, krige, vario_estimate_unstructured
from gstools.covmodel.plot import plot_variogram
import matplotlib.pyplot as plt
import numpy as np
import omfvista
import pooch
import pyvista as pv

Load the Data

For this project, we have two data archives in the Open Mining Format (OMF) specification and we will use one of PyVista’s companion projects, omfvista to load those data archives into PyVista a MultiBlock dataset.

url = "https://dl.dropbox.com/s/3cuxvurj8zubchb/FORGE.omf?dl=0"
path = pooch.retrieve(url=url, known_hash=None)

project = omfvista.load_project(path)
project
InformationBlocks
MultiBlockValues
N Blocks7
X Bounds329924.988, 344152.930
Y Bounds4252833.482, 4270951.098
Z Bounds-2606.000, 2906.000
IndexNameType
0TerrainPolyData
1Opal Mound FaultPolyData
2Negro Mag FaultPolyData
3Top of BasementPolyData
4Site BoundaryPolyData
5Observed TemperaturePolyData
6Observed GravityPolyData


Initial Inspection

Now we can go ahead and create an integrated visualization of all of the data available to us.

p = pv.Plotter(window_size=np.array([1024, 768]) * 2)
p.add_mesh(project["Site Boundary"], color="yellow", render_lines_as_tubes=True, line_width=10)
p.add_mesh(project["Terrain"], opacity=0.7, lighting=False)
p.add_mesh(project["Opal Mound Fault"], color="brown", opacity=0.7)
p.add_mesh(project["Negro Mag Fault"], color="lightblue", opacity=0.7)
p.add_mesh(
    project["Observed Temperature"],
    cmap="coolwarm",
    clim=[10, 270],
    point_size=15,
    render_points_as_spheres=True,
)

p.camera_position = [
    (315661.9406719345, 4234675.528454831, 15167.291249498076),
    (337498.00521202036, 4260818.504034578, -1261.5688408692681),
    (0.2708862567924439, 0.3397398234107863, 0.9006650255615491),
]
p.show()
kriging

Kriging

Now we will use an external library, [gstools](https://geostat-framework.github.io), to krige the temperature probe data into a 3D temperature model of the subsurface.

Choosing a Model Space

We start out by creating the 3D model space as a PyVista UniformGrid that encompasses the project region. The following were chosen manually:

# Create the kriging grid
grid = pv.UniformGrid()
# Bottom south-west corner
grid.origin = (329700, 4252600, -2700)
# Cell sizes
grid.spacing = (250, 250, 50)
# Number of cells in each direction
grid.dimensions = (60, 75, 100)
/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pyvista/core/grid.py:912: PyVistaDeprecationWarning: `UniformGrid` is deprecated. Use `ImageData` instead.
  warnings.warn(

Visually inspect the kriging grid in relation to data

p = pv.Plotter(window_size=np.array([1024, 768]) * 2)
p.add_mesh(grid, opacity=0.5, color=True)
p.add_mesh(project["Terrain"], opacity=0.75)
p.add_mesh(
    project["Observed Temperature"],
    cmap="coolwarm",
    point_size=15,
    render_points_as_spheres=True,
    scalar_bar_args={"title": "Temperature (C)"},
)
p.camera_position = [
    (303509.4197523619, 4279629.689766085, 8053.049483835099),
    (336316.405356571, 4261479.748583805, -1756.358124546427),
    (0.22299463811939535, -0.11978828465250713, 0.9674317331109259),
]
p.show()
kriging

Variogram Analysis

Next, we need to compute a variogram for the temperature probe data and fit that variogram to an exponential model

bins = np.linspace(0, 12300, 1000)
bin_center, gamma = vario_estimate_unstructured(
    project["Observed Temperature"].points.T,
    project["Observed Temperature"]["temperature (C)"],
    bins,
)


fit_model = Exponential(dim=3)
fit_model.fit_variogram(bin_center, gamma, nugget=False)

plt.figure(figsize=(10, 5))
plt.plot(bin_center, gamma)
plot_variogram(fit_model, x_max=bins[-1], ax=plt.gca())
plt.xlabel("Lag Distance")
plt.ylabel("Variogram")
plt.show()
kriging

Performing the Kriging

Then we pass the fitted exponential model when instantiating the kriging operator from GSTools.

# Create the kriging model
krig = krige.Ordinary(
    fit_model,
    project["Observed Temperature"].points.T,
    project["Observed Temperature"]["temperature (C)"],
)

After instantiating the kriging operator, we can have it operate on the nodes of our 3D grid that we created earlier and collect the results back onto the grid.

krig.mesh(
    grid,
    name="temperature (C)",
)
(array([116.47283434, 117.61429646, 118.68575325, ...,  38.14187354,
        39.55052588,  40.95368015]), array([8097.97806581, 7671.55310286, 7233.87779749, ..., 8755.22696965,
       9108.52838828, 9445.1394835 ]))

And now the grid model has the temperature scalar field and kriging variance as data arrays.

project["Kriged Temperature Model"] = grid
project
InformationBlocks
MultiBlockValues
N Blocks8
X Bounds329700.000, 344450.000
Y Bounds4252600.000, 4271100.000
Z Bounds-2700.000, 2906.000
IndexNameType
0TerrainPolyData
1Opal Mound FaultPolyData
2Negro Mag FaultPolyData
3Top of BasementPolyData
4Site BoundaryPolyData
5Observed TemperaturePolyData
6Observed GravityPolyData
7Kriged Temperature ModelUniformGrid


Post-Processing

We can use a filter from PVGeo to extract the region of the temperature model that is beneath the topography surface as the kriging did not account for the surface boundary.

# Instantiate the algorithm
extractor = PVGeo.grids.ExtractTopography(tolerance=10, remove=True)
# Apply the algorithm to the PyVista grid using the topography surface
subsurface = extractor.apply(grid, project["Terrain"])

Resulting Visualization

# Create the main figure
def add_contents(p):
    """A helper to add data to scenes."""
    p.add_mesh(
        project["Site Boundary"].tube(50),
        color="yellow",
    )
    p.add_mesh(project["Terrain"], opacity=0.7, lighting=False)

    p.add_mesh(project["Opal Mound Fault"], color="brown", opacity=0.75)
    p.add_mesh(project["Negro Mag Fault"], color="lightblue", opacity=0.75)

    p.add_mesh(
        subsurface.ctp().contour([175, 225], scalars="temperature (C)"),
        name="the model",
        scalars="temperature (C)",
        cmap="coolwarm",
        clim=[10, 270],
        opacity=0.9,
        scalar_bar_args={"title": "Temperature (C)"},
    )
    p.add_mesh(
        project["Observed Temperature"],
        cmap="coolwarm",
        clim=[10, 270],
        render_points_as_spheres=True,
        point_size=10,
        scalar_bar_args={"title": "Temperature (C)"},
    )

    return


p = pv.Plotter()

add_contents(p)

p.camera_position = [
    (319663.46494887985, 4294870.4704494225, -8787.973684799075),
    (336926.4650625, 4261892.29012, 103.0),
    (-0.09983848586767283, 0.20995262898057362, 0.9726007250273854),
]

p.show()
kriging

Total running time of the script: (1 minutes 48.193 seconds)

Gallery generated by Sphinx-Gallery