Horizon Modeling¶
A horizon is a continuous subsurface surface defined as \(z = f(x, y)\), where the value represents depth or elevation across the model domain.
In Petres, horizons are constructed from sampled \((x, y, z)\) data
using the Horizon class and reconstructed via
interpolation. Once defined, the surface can be evaluated at any
location and visualized over arbitrary grids.
Note
A horizon is a continuous surface, not a grid. It is evaluated on demand using an interpolator and does not contain inherent discretization.
Creating a Horizon¶
Horizons in Petres can be created in two ways: from sample points or from well tops.
From Sample Points¶
You can define a horizon directly from smaple points using an interpolator. The following example creates a horizon from four sample points using Inverse Distance Weighting (IDW) interpolation.
from petres.interpolators import IDWInterpolator
from petres.models import Horizon
horizon1 = Horizon(
name="H1",
xy=[[0, 0], [100, 0], [100, 100], [0, 100]],
depth=[0, 1, 0, 1],
interpolator=IDWInterpolator(),
)
Here, name defines the name of horizon,
xy specifies the spatial coordinates of the sample points,
depth provides their corresponding depth (or elevation) values,
and interpolator controls how the continuous surface is reconstructed from these data.
Note
The choice of interpolator affects the geometry of the surface. See Interpolators page for more details and available options.
Important
Each horizon requires its own Interpolator instance. Do not reuse the same interpolator object across different horizons. This ensures that each surface is interpolated independently and correctly.
From Well Tops¶
Horizons can also be generated from well data. When creating a well, you can optionally define tops where it intersects horizons:
from petres.models import VerticalWell
well1 = VerticalWell(name="Well 1", x=20, y=78, tops={"Horizon 1": 100})
well2 = VerticalWell(name="Well 2", x=20, y=78, tops={"Horizon 1": 110})
well3 = VerticalWell(name="Well 3", x=32, y=55, tops={"Horizon 1": 90})
Here, name is the well’s identifier,
x and y define its location,
and tops is a dictionary where the keys are
horizon names and the values are the corresponding depth at that well.
Multiple horizons can be defined in the same dictionary.
Tops can also be added after well creation using the add_top() method:
well1.add_top("Horizon 2", 100)
well2.add_top("Horizon 2", 110)
well3.add_top("Horizon 2", 90)
Where the first argument is the horizon name and the second is the depth value.
Note
Horizon names must be unique. Reusing an existing name will raise an error.
Once wells are ready, create a horizon using their tops:
horizon = Horizon.from_wells(
name="Horizon 1",
wells=[well1, well2, well3],
interpolator=IDWInterpolator()
)
Note
Wells must have a top defined for the same horizon name; otherwise, horizon creation will fail.
After the horizon is created, you can find the depth where it intersects any well, even if the well was not used for interpolation:
print(horizon.intersect(well2))
Visualizing a Horizon¶
Once a horizon has been defined in Petres, it can be quickly visualized using the
show() method. This provides an immediate view of the horizon surface, helping
to inspect its shape.
Horizon surfaces are mathematically continuous, defined as \(z = f(x, y)\). However, computers can only represent a finite number of points. To visualize a horizon, the continuous surface is therefore sampled on a discrete grid. Petres offers multiple ways to define this sampling grid.
Direct Coordinate Sampling¶
You can specify the exact \(x\) and \(y\) coordinates to define the spatial sampling grid:
horizon1.show(
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
)
Sampling with Limits and Resolution¶
Instead of providing coordinates directly, you can define the grid using limits and the number of points along each axis:
horizon1.show(
xlim=(0, 100),
ylim=(0, 100),
ni=50,
nj=50,
)
Sampling with Grid Spacing¶
Alternatively, you can specify the desired spacing between grid points along each axis:
horizon1.show(
xlim=(0, 100),
ylim=(0, 100),
dx=2,
dy=2,
)
Note
All these approaches differ only in how the sampling grid is defined for visualization purposes. The underlying horizon surface is always evaluated using the same interpolated model, regardless of how the visualization grid is specified.
By default, show() renders the horizon surface in 3D.
However, the view argument allows switching between 2D and 3D visualizations.
To visualize the horizon in 2D, set view="2d":
horizon1.show(
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
view="2d"
)
For more advanced control over the visualization, Petres provides separate
show2d() and show3d() methods,
which offer additional customization options for 2D and 3D plots, respectively.
Wells can also be visualized alongside horizons by passing them to the wells argument:
horizon1.show(
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
wells=[well1, well2, well3]
)
Visualizing Multiple Horizons¶
Multiple horizons can be displayed together using
Viewer3D.
from petres.viewers import Viewer3D
viewer = Viewer3D()
viewer.add_horizons(
horizons=[horizon1, horizon2, horizon3],
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
cmap="viridis",
)
viewer.show()
Note
The same grid sampling options described previously for show() are available here.
Manual Color Assignment¶
viewer = Viewer3D()
viewer.add_horizon(
horizon1,
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
color="red",
)
viewer.add_horizon(
horizon2,
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
color="blue",
)
viewer.add_horizon(
horizon3,
x=np.linspace(0, 100, 50),
y=np.linspace(0, 100, 50),
color="green",
)
viewer.show()
Important
Horizons are reusable objects and can be used to define multiple zones (see Creating a Zone).