-
Notifications
You must be signed in to change notification settings - Fork 76
Update PGS to contain tree based lithotype rules #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6240382
c98efc2
6ed4dfa
de6b3dc
62a96ff
34f9f85
872130f
fc1bd97
5c749b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
""" | ||
PGS through decision trees | ||
-------------------------- | ||
|
||
In typical PGS workflows, the lithotype is defined through a spatial rule. A | ||
more flexible approach can be taken such that the lithotype is represented as a | ||
decision tree. More specifically, this is given as a binary tree, where each | ||
node is a decision based on the values of the spatial random | ||
fields [Sektnan et al., 2024](https://doi.org/10.1007/s11004-024-10162-5). The | ||
leaf nodes are then assigned a discrete value which is given to the cell that | ||
is being evaluated. Here, a simple example is provided showing how to use the | ||
tree based approach in conducting plurigaussian simulation. | ||
|
||
As in the previous examples, we define the simulation domain and generate the | ||
necessary spatial random fields. | ||
""" | ||
|
||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
import gstools as gs | ||
|
||
dim = 2 | ||
|
||
# no. of cells in both dimensions | ||
N = [150, 150] | ||
|
||
x = np.arange(N[0]) | ||
y = np.arange(N[1]) | ||
|
||
model = gs.Gaussian(dim=dim, var=1, len_scale=10) | ||
srf = gs.SRF(model) | ||
field1 = srf.structured([x, y], seed=215253419) | ||
field2 = srf.structured([x, y], seed=192534221) | ||
|
||
############################################################################### | ||
# Decisions within the tree are carried out through user defined functions. | ||
# In this way, the lithotype is defined in a continuous space, not relying on | ||
# discretization. In this example we will use an ellipse as a decision boundary. | ||
# The function accepts a data dictionary, which contains the values of the | ||
# spatial random fields, and the parameters of the ellipse. | ||
|
||
|
||
def ellipse(data, key1, key2, c1, c2, s1, s2, angle=0): | ||
x, y = data[key1] - c1, data[key2] - c2 | ||
|
||
if angle: | ||
theta = np.deg2rad(angle) | ||
c, s = np.cos(theta), np.sin(theta) | ||
x, y = x * c + y * s, -x * s + y * c | ||
|
||
return (x / s1) ** 2 + (y / s2) ** 2 <= 1 | ||
Comment on lines
+44
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be a great idea to put functions like |
||
|
||
|
||
############################################################################### | ||
# The decision tree is defined as a dictionary, where each node is a dictionary | ||
# itself. The root node is the first decision, which branches into two nodes, | ||
# one for each possible outcome. The leaf nodes are the final decisions, which | ||
# assign a discrete value to the given cell. The `func` key in each decision | ||
# node contains the function to be called, and the `args` key contains the | ||
# arguments to be passed to the function. These arguments must match the | ||
# parameters of the function. The `yes_branch` and `no_branch` keys contain the | ||
# names of the nodes to follow based on the outcome of the decision. `root` | ||
# must be given, but all other node names are arbitrary. | ||
# | ||
# Here, we define a simple decision tree with two phases. The first node | ||
# checks if the point is inside an ellipse, and if it is, it assigns the value | ||
# 1 to the lithotype. If it is not, it assigns the value 0. The parameters | ||
# of the ellipse are defined in the `args` dictionary. The keys `key1` and | ||
# `key2` refer to the spatial random fields, which are used to define the | ||
# ellipse. In the algorithm, the fields are indexed as `Z1`, `Z2`, etc., | ||
# depending on the order in which they are passed to the PGS object. In this | ||
# case, `Z1` refers to `field1` and `Z2` refers to `field2`. The parameters | ||
# `c1`, `c2`, `s1`, `s2`, and `angle` define the center, scale, and rotation of | ||
# the ellipse, respectively. | ||
# | ||
# Lithotype decision functions operate in the latent Gaussian domain [-3,3]×[-3,3]. | ||
# Since each GRF is N(0,1), approximately 99.7% of its values lie within ±3σ, | ||
# making ±3 a natural “full” range for defining splits or shapes. | ||
|
||
config = { | ||
"root": { | ||
"type": "decision", | ||
"func": ellipse, | ||
"args": { | ||
"key1": "Z1", | ||
"key2": "Z2", | ||
"c1": 0, | ||
"c2": 0, | ||
"s1": 2.5, | ||
"s2": 0.8, | ||
"angle": -45, | ||
}, | ||
"yes_branch": "phase1", | ||
"no_branch": "phase0", | ||
}, | ||
"phase1": {"type": "leaf", "action": 1}, | ||
"phase0": {"type": "leaf", "action": 0}, | ||
} | ||
|
||
############################################################################### | ||
# With the tree configuration ready, we can create the PGS object as normal, | ||
# passing our domain size and spatial random fields. When generating the | ||
# plurigaussian field, we pass the tree configuration so GSTools knows which | ||
# PGS process to follow. | ||
|
||
pgs = gs.PGS(dim, [field1, field2]) | ||
|
||
P = pgs(tree=config) | ||
|
||
############################################################################### | ||
# We can also compute the equivalent spatial lithotype. | ||
# | ||
# NB: If we want to compute L before P, we must pass the tree config to | ||
# `pgs.compute_lithotype(tree=config)`. | ||
|
||
L = pgs.compute_lithotype() | ||
|
||
############################################################################### | ||
# Finally, we plot `P` as well as the equivalent spatial lithotype `L`. | ||
|
||
fig, axs = plt.subplots(1, 2, figsize=(10, 5), dpi=150) | ||
|
||
im0 = axs[0].imshow(L, cmap="copper", origin="lower") | ||
axs[0].set_title("L") | ||
im1 = axs[1].imshow(P, cmap="copper", origin="lower") | ||
axs[1].set_title("P") | ||
|
||
############################################################################### | ||
# | ||
# .. image:: ../../pics/2d_tree_pgs.png | ||
# :width: 400px | ||
# :align: center |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
""" | ||
From bigaussian to plurigaussian simulation | ||
-------------------------------------------------- | ||
|
||
In many PGS implementations, the dimensions of the simulation domain often | ||
matches the number of fields that are supplied. However, this is not a | ||
requirement of the PGS algorithm. In fact, it is possible to use multiple | ||
spatial random fields in PGS, which can be useful for more complex lithotype | ||
definitions. In this example, we will demonstrate how to use multiple SRFs in | ||
PGS. In GSTools, this is done by utlising the tree based architecture. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. spelling |
||
|
||
Typically, PGS in two dimensions is carried out as a bigaussian simulation, | ||
where two random fields are used. Here, we will employ four. We begin by | ||
setting up the simulation domain and generating the necessary random fields, | ||
where the length scales of two of the fields are much larger than the other two. | ||
""" | ||
|
||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
import gstools as gs | ||
|
||
dim = 2 | ||
|
||
N = [200, 200] | ||
|
||
x = np.arange(N[0]) | ||
y = np.arange(N[1]) | ||
|
||
model = gs.Gaussian(dim=dim, var=1, len_scale=15) | ||
srf = gs.SRF(model) | ||
field1 = srf.structured([x, y], seed=201519) | ||
field2 = srf.structured([x, y], seed=199221) | ||
model = gs.Gaussian(dim=dim, var=1, len_scale=3) | ||
srf = gs.SRF(model) | ||
field3 = srf.structured([x, y], seed=1345134) | ||
field4 = srf.structured([x, y], seed=1351455) | ||
|
||
############################################################################### | ||
# As in the previous example, an ellipse is used as the decision boundary. | ||
|
||
|
||
def ellipse(data, key1, key2, c1, c2, s1, s2, angle=0): | ||
x, y = data[key1] - c1, data[key2] - c2 | ||
|
||
if angle: | ||
theta = np.deg2rad(angle) | ||
c, s = np.cos(theta), np.sin(theta) | ||
x, y = x * c + y * s, -x * s + y * c | ||
|
||
return (x / s1) ** 2 + (y / s2) ** 2 <= 1 | ||
|
||
|
||
############################################################################### | ||
# The configuration dictionary for the decision tree is defined as before, but | ||
# this time we pass the additional keys `Z3` and `Z4`, which refer to the | ||
# additional fields `field3` and `field4`. The decision tree is structured in a | ||
# way that the first decision node is based on the first two fields, and the | ||
# second decision node is based on the last two fields. | ||
|
||
config = { | ||
"root": { | ||
"type": "decision", | ||
"func": ellipse, | ||
"args": { | ||
"key1": "Z1", | ||
"key2": "Z2", | ||
"c1": 0.7, | ||
"c2": 0.7, | ||
"s1": 1.3, | ||
"s2": 1.3, | ||
}, | ||
"yes_branch": "phase1", | ||
"no_branch": "node1", | ||
}, | ||
"node1": { | ||
"type": "decision", | ||
"func": ellipse, | ||
"args": { | ||
"key1": "Z3", | ||
"key2": "Z4", | ||
"c1": -0.7, | ||
"c2": -0.7, | ||
"s1": 1.3, | ||
"s2": 1.3, | ||
"angle": 30, | ||
}, | ||
"yes_branch": "phase2", | ||
"no_branch": "phase0", | ||
}, | ||
"phase2": {"type": "leaf", "action": 2}, | ||
"phase1": {"type": "leaf", "action": 1}, | ||
"phase0": {"type": "leaf", "action": 0}, | ||
} | ||
|
||
############################################################################### | ||
# Next, we create the PGS object, passing in all four fields, and generate the | ||
# plurigaussian field `P` | ||
|
||
pgs = gs.PGS(dim, [field1, field2, field3, field4]) | ||
|
||
P = pgs(tree=config) | ||
|
||
############################################################################### | ||
# Finally, we plot `P` | ||
# | ||
# NB: In the current implementation, the calculation of the equivalent spatial | ||
# lithotype `L` is not supported for multiple fields | ||
|
||
plt.figure(figsize=(8, 6)) | ||
plt.imshow(P, cmap="copper", origin="lower") | ||
|
||
############################################################################### | ||
# | ||
# .. image:: ../../pics/2d_multi_tree_pgs.png | ||
# :width: 400px | ||
# :align: center |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
""" | ||
Three dimensional PGS through decision trees | ||
-------------------------------------------- | ||
|
||
Let's apply the decision tree approach to three dimensional PGS | ||
""" | ||
|
||
import numpy as np | ||
|
||
import gstools as gs | ||
|
||
dim = 3 | ||
|
||
# no. of cells in all dimensions | ||
N = [60] * dim | ||
|
||
x = np.arange(N[0]) | ||
y = np.arange(N[1]) | ||
z = np.arange(N[2]) | ||
|
||
############################################################################### | ||
# As we want to generate a three dimensional field, we generate three spatial | ||
# random fields (SRF) as our input. In this example, the number of fields | ||
# is equal to the number of dimensions, but as before, this is not a | ||
# requirement. | ||
|
||
model = gs.Gaussian(dim=dim, var=1, len_scale=[15, 15, 15]) | ||
srf = gs.SRF(model) | ||
field1 = srf.structured([x, y, z], seed=20277519) | ||
field2 = srf.structured([x, y, z], seed=19727221) | ||
field3 = srf.structured([x, y, z], seed=21145612) | ||
|
||
############################################################################### | ||
# The decision tree will now utilise an ellipsoid as the decision boundary, | ||
# having a similar structure as the ellipse in the previous example. | ||
|
||
|
||
def ellipsoid(data, key1, key2, key3, c1, c2, c3, s1, s2, s3): | ||
return ((data[key1] - c1) / s1) ** 2 + ((data[key2] - c2) / s2) ** 2 + ( | ||
(data[key3] - c3) / s3 | ||
) ** 2 <= 1 | ||
|
||
|
||
config = { | ||
"root": { | ||
"type": "decision", | ||
"func": ellipsoid, | ||
"args": { | ||
"key1": "Z1", | ||
"key2": "Z2", | ||
"key3": "Z3", | ||
"c1": 0, | ||
"c2": 0, | ||
"c3": 0, | ||
"s1": 3, | ||
"s2": 1, | ||
"s3": 0.4, | ||
}, | ||
"yes_branch": "phase1", | ||
"no_branch": "phase0", | ||
}, | ||
"phase0": {"type": "leaf", "action": 0}, | ||
"phase1": {"type": "leaf", "action": 1}, | ||
} | ||
|
||
############################################################################### | ||
# As before, we initialise the PGS process, generate `P`, and plot to | ||
# visualise the results | ||
|
||
import pyvista as pv | ||
|
||
pgs = gs.PGS(dim, [field1, field2, field3]) | ||
P = pgs(tree=config) | ||
|
||
grid = pv.ImageData(dimensions=N) | ||
grid.point_data["PGS"] = P.reshape(-1) | ||
grid.threshold(0.5, scalars="PGS").plot() | ||
|
||
############################################################################### | ||
# | ||
# .. image:: ../../pics/3d_tree_pgs.png | ||
# :width: 400px | ||
# :align: center |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,27 @@ | ||
Plurigaussian Field Generation | ||
============================== | ||
Plurigaussian Simulation | ||
======================== | ||
|
||
Plurigaussian field simulations (PGS) are used to simulate correlated fields | ||
Plurigaussian simulation (PGS) is used to simulate correlated fields | ||
of categorical data, e.g. lithofacies, hydrofacies, soil types, or | ||
cementitious materials. | ||
PGS uses one spatial random field (SRF) per dimension, e.g. two SRFs, when | ||
working with two dimensional data. Furthermore, PGS needs a field, which | ||
describes the categorical data and its spatial relations. | ||
This might sound more complicated then it is, as we will see in the following | ||
examples. | ||
In general, we define a categorical rule which dictates the relative | ||
frequency and connectivity of the phases present in the final realisation. | ||
We employ spatial random fields (SRFs) to map to this rule. | ||
This mapping determines the phase of a given point in the simulation domain. | ||
The definition of this rule limits how we can interact with it. | ||
For example, the rule may be defined spatially (e.g. as an image or array) | ||
or via a decision tree. Both forms will be explored in the following | ||
examples, highlighting their differences. | ||
Many PGS approaches constrain the number of input SRFs to match the | ||
dimensions of the simulation domain. This constraint stems from the | ||
implementation, not the method itself. | ||
With a spatial lithotype, we perform bigaussian and trigaussian | ||
simulations for two- and three-dimensional realisations, respectively. | ||
In contrast, the tree-based approach allows an arbitrary number of SRFs, | ||
yielding a fully *pluri*gaussian simulation. | ||
This may sound more complicated than it is; we will clarify everything | ||
in the examples that follow. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for this text :-) |
||
|
||
Examples | ||
-------- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this doesn't work in sphinx. I just realised that I made the same mistake in my own PGS examples. I'll have to fix those. The correct syntax is