Skip to content

Commit 7da6000

Browse files
authoredApr 27, 2025
Merge pull request #370 from GeoStat-Framework/pgs
Plurigaussian fields
2 parents bfcb495 + 0464dbc commit 7da6000

23 files changed

+1180
-6
lines changed
 

‎README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ GSTools provides geostatistical tools for various purposes:
4343
- data normalization and transformation
4444
- many readily provided and even user-defined covariance models
4545
- metric spatio-temporal modelling
46+
- plurigaussian field simulations (PGS)
4647
- plotting and exporting routines
4748

4849

@@ -110,6 +111,7 @@ The documentation also includes some [tutorials][tut_link], showing the most imp
110111
- [Geographic Coordinates][tut8_link]
111112
- [Spatio-Temporal Modelling][tut9_link]
112113
- [Normalizing Data][tut10_link]
114+
- [Plurigaussian Field Generation (PGS)][tut11_link]
113115
- [Miscellaneous examples][tut0_link]
114116

115117
The associated python scripts are provided in the `examples` folder.
@@ -321,6 +323,51 @@ yielding
321323
[kraichnan_link]: https://doi.org/10.1063/1.1692799
322324

323325

326+
## Plurigaussian Field Simulation (PGS)
327+
328+
With PGS, more complex categorical (or discrete) fields can be created.
329+
330+
331+
### Example
332+
333+
```python
334+
import gstools as gs
335+
import numpy as np
336+
import matplotlib.pyplot as plt
337+
338+
N = [180, 140]
339+
340+
x, y = range(N[0]), range(N[1])
341+
342+
# we need 2 SRFs
343+
model = gs.Gaussian(dim=2, var=1, len_scale=5)
344+
srf = gs.SRF(model)
345+
field1 = srf.structured([x, y], seed=20170519)
346+
field2 = srf.structured([x, y], seed=19970221)
347+
348+
# with `lithotypes`, we prescribe the categorical data and its relations
349+
# here, we use 2 categories separated by a rectangle.
350+
rect = [40, 32]
351+
lithotypes = np.zeros(N)
352+
lithotypes[
353+
N[0] // 2 - rect[0] // 2 : N[0] // 2 + rect[0] // 2,
354+
N[1] // 2 - rect[1] // 2 : N[1] // 2 + rect[1] // 2,
355+
] = 1
356+
357+
pgs = gs.PGS(2, [field1, field2])
358+
P = pgs(lithotypes)
359+
360+
fig, axs = plt.subplots(1, 2)
361+
axs[0].imshow(lithotypes, cmap="copper")
362+
axs[1].imshow(P, cmap="copper")
363+
plt.show()
364+
```
365+
366+
<p align="center">
367+
<img src="https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/2d_pgs.png" alt="PGS" width="600px"/>
368+
</p>
369+
370+
324371
## VTK/PyVista Export
325372

326373
After you have created a field, you may want to save it to file, so we provide
@@ -393,6 +440,7 @@ You can contact us via <info@geostat-framework.org>.
393440
[tut8_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/08_geo_coordinates/index.html
394441
[tut9_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/09_spatio_temporal/index.html
395442
[tut10_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/10_normalizer/index.html
443+
[tut11_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/11_plurigaussian/index.html
396444
[tut0_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/00_misc/index.html
397445
[cor_link]: https://en.wikipedia.org/wiki/Autocovariance#Normalization
398446
[vtk_link]: https://www.vtk.org/

‎docs/source/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def setup(app):
300300
"../../examples/08_geo_coordinates/",
301301
"../../examples/09_spatio_temporal/",
302302
"../../examples/10_normalizer/",
303+
"../../examples/11_plurigaussian/",
303304
],
304305
# path where to save gallery generated examples
305306
"gallery_dirs": [
@@ -314,6 +315,7 @@ def setup(app):
314315
"examples/08_geo_coordinates/",
315316
"examples/09_spatio_temporal/",
316317
"examples/10_normalizer/",
318+
"examples/11_plurigaussian/",
317319
],
318320
# Pattern to search for example files
319321
"filename_pattern": r"\.py",

‎docs/source/index.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ GSTools provides geostatistical tools for various purposes:
3333
- data normalization and transformation
3434
- many readily provided and even user-defined covariance models
3535
- metric spatio-temporal modelling
36+
- plurigaussian field simulations (PGS)
3637
- plotting and exporting routines
3738

3839

@@ -164,6 +165,7 @@ showing the most important use cases of GSTools, which are
164165
- `Geographic Coordinates <examples/08_geo_coordinates/index.html>`__
165166
- `Spatio-Temporal Modelling <examples/09_spatio_temporal/index.html>`__
166167
- `Normalizing Data <examples/10_normalizer/index.html>`__
168+
- `Plurigaussian Field Generation (PGS) <examples/11_plurigaussian/index.html>`__
167169
- `Miscellaneous examples <examples/00_misc/index.html>`__
168170

169171

@@ -391,6 +393,53 @@ yielding
391393
:align: center
392394

393395

396+
Plurigaussian Field Simulation (PGS)
397+
====================================
398+
399+
With PGS, more complex categorical (or discrete) fields can be created.
400+
401+
402+
Example
403+
-------
404+
405+
.. code-block:: python
406+
407+
import gstools as gs
408+
import numpy as np
409+
import matplotlib.pyplot as plt
410+
411+
N = [180, 140]
412+
413+
x, y = range(N[0]), range(N[1])
414+
415+
# we need 2 SRFs
416+
model = gs.Gaussian(dim=2, var=1, len_scale=5)
417+
srf = gs.SRF(model)
418+
field1 = srf.structured([x, y], seed=20170519)
419+
field2 = srf.structured([x, y], seed=19970221)
420+
421+
# with `lithotypes`, we prescribe the categorical data and its relations
422+
# here, we use 2 categories separated by a rectangle.
423+
rect = [40, 32]
424+
lithotypes = np.zeros(N)
425+
lithotypes[
426+
N[0] // 2 - rect[0] // 2 : N[0] // 2 + rect[0] // 2,
427+
N[1] // 2 - rect[1] // 2 : N[1] // 2 + rect[1] // 2,
428+
] = 1
429+
430+
pgs = gs.PGS(2, [field1, field2])
431+
P = pgs(lithotypes)
432+
433+
fig, axs = plt.subplots(1, 2)
434+
axs[0].imshow(lithotypes, cmap="copper")
435+
axs[1].imshow(P, cmap="copper")
436+
plt.show()
437+
438+
.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/2d_pgs.png
439+
:width: 600px
440+
:align: center
441+
442+
394443
VTK/PyVista Export
395444
==================
396445

‎docs/source/pics/2d_pgs.png

68.3 KB
Loading

‎docs/source/pics/3d_pgs.png

137 KB
Loading

‎docs/source/tutorials.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ explore its whole beauty and power.
2222
examples/08_geo_coordinates/index
2323
examples/09_spatio_temporal/index
2424
examples/10_normalizer/index
25+
examples/11_plurigaussian/index
2526
examples/00_misc/index
2627

2728
.. only:: html

‎examples/00_misc/01_export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
###############################################################################
2121
# The result displayed with Paraview:
2222
#
23-
# .. image:: https://raw.githubusercontent.com/GeoStat-Framework/GeoStat-Framework.github.io/master/img/paraview.png
23+
# .. image:: ../../pics/paraview.png
2424
# :width: 400px
2525
# :align: center

‎examples/01_random_field/06_pyvista_support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@
5353
###############################################################################
5454
# The result should look like this:
5555
#
56-
# .. image:: https://github.com/GeoStat-Framework/GeoStat-Framework.github.io/raw/master/img/GS_pyvista_cut.png
56+
# .. image:: ../../pics/GS_pyvista_cut.png
5757
# :width: 400px
5858
# :align: center

‎examples/04_vector_field/01_3d_vector_field.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@
5959
###############################################################################
6060
# The result should look like this:
6161
#
62-
# .. image:: https://github.com/GeoStat-Framework/GeoStat-Framework.github.io/raw/master/img/GS_3d_vector_field.png
62+
# .. image:: ../../pics/GS_3d_vector_field.png
6363
# :width: 400px
6464
# :align: center
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
A First and Simple Example
3+
--------------------------
4+
5+
As a first example, we will create a two dimensional plurigaussian field
6+
(PGS). Thus, we need two spatial random fields(SRF) and on top of that, we
7+
need a field describing the categorical data and its spatial relation.
8+
We will start off by creating the two SRFs with a Gaussian variogram, which
9+
makes the fields nice and smooth. But before that, we will import all
10+
necessary libraries and define a few variables, like the number of grid
11+
cells in each dimension.
12+
"""
13+
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
17+
import gstools as gs
18+
19+
dim = 2
20+
# no. of cells in both dimensions
21+
N = [180, 140]
22+
23+
x = np.arange(N[0])
24+
y = np.arange(N[1])
25+
26+
###############################################################################
27+
# In this first example we will use the same geostatistical parameters for
28+
# both fields for simplicity. Thus, we can use the same SRF instance for the
29+
# two fields.
30+
31+
model = gs.Gaussian(dim=dim, var=1, len_scale=10)
32+
srf = gs.SRF(model)
33+
field1 = srf.structured([x, y], seed=20170519)
34+
field2 = srf.structured([x, y], seed=19970221)
35+
36+
###############################################################################
37+
# Now, we will create the lithotypes field describing the categorical data. For
38+
# now, we will only have two categories and we will address them by the
39+
# integers 0 and 1. We start off by creating a matrix of 0s from which we will
40+
# select a rectangle and fill that with 1s. This field does not have to match
41+
# the shape of the SRFs.
42+
43+
centroid = [200, 160]
44+
45+
# size of the rectangle
46+
rect = [40, 32]
47+
48+
lithotypes = np.zeros(centroid)
49+
lithotypes[
50+
centroid[0] // 2 - rect[0] // 2 : centroid[0] // 2 + rect[0] // 2,
51+
centroid[1] // 2 - rect[1] // 2 : centroid[1] // 2 + rect[1] // 2,
52+
] = 1
53+
54+
###############################################################################
55+
# With the two SRFs and the L-field ready, we can create our first PGS. First, we
56+
# set up an instance of the PGS class and then we are ready to calculate the
57+
# field by calling the instance and handing over the L-field.
58+
59+
pgs = gs.PGS(dim, [field1, field2])
60+
P = pgs(lithotypes)
61+
62+
###############################################################################
63+
# Finally, we can plot the PGS, but we will also show the L-field and the two
64+
# original Gaussian fields.
65+
66+
fig, axs = plt.subplots(2, 2)
67+
68+
axs[0, 0].imshow(field1, cmap="copper", origin="lower")
69+
axs[0, 1].imshow(field2, cmap="copper", origin="lower")
70+
axs[1, 0].imshow(lithotypes, cmap="copper", origin="lower")
71+
axs[1, 1].imshow(P, cmap="copper", origin="lower")
72+
73+
# For more information on Plurigaussian fields and how they naturally extend
74+
# truncated Gaussian fields, we can recommend the book
75+
# [Plurigaussian Simulations in Geosciences](https://doi.org/10.1007/978-3-642-19607-2)

‎examples/11_plurigaussian/01_pgs.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Understanding PGS
3+
-----------------
4+
5+
In this example we want to try to understand how exactly PGS are generated
6+
and how to influence them with the categorical field.
7+
First of all, we will set everything up very similar to the first example.
8+
"""
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
import gstools as gs
14+
15+
dim = 2
16+
# no. of cells in both dimensions
17+
N = [100, 80]
18+
19+
x = np.arange(N[0])
20+
y = np.arange(N[1])
21+
22+
###############################################################################
23+
# In this example we will use different geostatistical parameters for the
24+
# SRFs. We will create fields with a strong anisotropy, and on top of that they
25+
# will both be rotated.
26+
27+
model1 = gs.Gaussian(dim=dim, var=1, len_scale=[20, 1], angles=np.pi / 8)
28+
srf1 = gs.SRF(model1, seed=20170519)
29+
field1 = srf1.structured([x, y])
30+
model2 = gs.Gaussian(dim=dim, var=1, len_scale=[1, 20], angles=np.pi / 4)
31+
srf2 = gs.SRF(model2, seed=19970221)
32+
field2 = srf2.structured([x, y])
33+
field1 += 5.0
34+
35+
###############################################################################
36+
# Internally, each field's values are mapped along an axis, which can be nicely
37+
# visualized with a scatter plot. We can easily do that by flattening the 2d
38+
# field values and simply use matplotlib's scatter plotting functionality.
39+
# The x-axis shows field1's values and the y-axis shows field2's values.
40+
41+
plt.scatter(field1.flatten(), field2.flatten(), s=0.1)
42+
43+
###############################################################################
44+
# This mapping always has a multivariate Gaussian distribution and this is also
45+
# the field on which we define our categorical data `lithotypes` and their
46+
# relations to each other. Before providing further explanations, we will
47+
# create the lithotypes field, which again will have only two categories, but
48+
# this time we will not prescribe a rectangle, but a circle.
49+
50+
# no. of grid cells of L-field
51+
M = [51, 41]
52+
# we need the indices of `lithotypes` later
53+
x_lith = np.arange(M[0])
54+
y_lith = np.arange(M[1])
55+
56+
# radius of circle
57+
radius = 7
58+
59+
lithotypes = np.zeros(M)
60+
mask = (x_lith[:, np.newaxis] - M[0] // 2) ** 2 + (
61+
y_lith[np.newaxis, :] - M[1] // 2
62+
) ** 2 < radius**2
63+
lithotypes[mask] = 1
64+
65+
###############################################################################
66+
# We can compute the actual PGS now. As a second step, we use a helper function
67+
# to recalculate the axes on which the lithotypes are defined. Normally, this
68+
# is handled internally. But in order to show the scatter plot together with
69+
# the lithotypes, we need the axes here.
70+
71+
pgs = gs.PGS(dim, [field1, field2])
72+
P = pgs(lithotypes)
73+
74+
x_lith, y_lith = pgs.calc_lithotype_axes(lithotypes.shape)
75+
76+
###############################################################################
77+
# And now to some plotting. Unfortunately, matplotlib likes to mess around with
78+
# the aspect ratios of the plots, so the left panel is a bit stretched.
79+
80+
fig, axs = plt.subplots(2, 2)
81+
axs[0, 0].imshow(field1, cmap="copper", origin="lower")
82+
axs[0, 1].imshow(field2, cmap="copper", origin="lower")
83+
axs[1, 0].scatter(field1.flatten(), field2.flatten(), s=0.1, color="C0")
84+
axs[1, 0].pcolormesh(x_lith, y_lith, lithotypes.T, alpha=0.3, cmap="copper")
85+
86+
axs[1, 1].imshow(P, cmap="copper", origin="lower")
87+
88+
###############################################################################
89+
# The black areas show the category 0 and the orange areas show category 1. We
90+
# see that the majority of all points in the scatter plot are within the
91+
# yellowish circle, thus the orange areas are larger than the black ones. The
92+
# strong anisotropy and the rotation of the fields create these interesting
93+
# patterns which remind us of fractures in the subsurface.

0 commit comments

Comments
 (0)