Property Modeling¶
This tutorial introduces property modeling in Petres. Grid properties are defined per cell and they represent scalar quantities such as porosity, permeability, saturation, net-to-gross, or any other spatially distributed field.
In this chapter, you will learn how to:
Create new grid properties
Fill them with constant or stochastic values
Derive one property from another
Populate values from existing NumPy arrays
Interpolate property values from wells
Assign properties zone by zone
Inspect statistics and summaries
Visualize the resulting distributions
Note
This tutorial assumes that you are already familiar with Horizon Modeling, Zone Modeling, Pillar Gridding, and grid creation workflows such as Grid Modeling from Horizons and Zones.
Creating a Property¶
The first step in property modeling is to create a property on the grid:
from petres.grids import CornerPointGrid
grid = CornerPointGrid.from_regular(
xlim=(0, 1000),
ylim=(0, 1000),
zlim=(0, 100),
ni=20,
nj=20,
nk=3,
)
porosity = grid.properties.create(
name="poro",
eclipse_keyword="PORO",
description="Porosity",
)
The name defines the internal Petres identifier used to access the property.
The object returned during creation is the same property and can be accessed later from the grid:
porosity = grid.properties["poro"]
The optional eclipse_keyword is useful when exporting the property to
Eclipse .GRDECL format. The description is also optional and can be used
to provide a more readable explanation of the property, if needed.
Constant Property Assignment¶
A property can be filled with a constant value across the whole grid using
fill() method:
porosity.fill(0.2)
This assigns a porosity value of 0.2 to every cell.
Visualizing a Property¶
A property can be visualized directly:
porosity.show()
By default, inactive cells are hidden. To include them:
porosity.show(show_inactive=True)
The colormap can be customized:
porosity.show(cmap="viridis")
Any valid Matplotlib colormap name can be used, such as "viridis",
"plasma", "coolwarm", or "inferno".
A title is automatically generated by default (e.g., "Property: poro").
You can override the title:
porosity.show(title="Porosity Distribution")
Or disable it entirely:
porosity.show(title=None)
Deriving a Property from Other Sources¶
A new property can be computed from one or more existing sources using
apply(). For example, permeability may be derived from porosity:
permeability = grid.properties.create(
name="perm",
eclipse_keyword="PERM",
description="Permeability",
)
permeability.apply(lambda poro: 100 * poro**3, source=porosity)
permeability.show()
The source argument may refer to:
An existing property object
The name of an existing property
One of the predefined grid-derived attributes
You can also refer to a property using its name:
permeability.apply(lambda poro: 100 * poro**3, source="poro")
Using a Named Function¶
Instead of a lambda expression, a regular Python function can be used.
def calc_perm(poro):
return 100 * poro**3
permeability.apply(calc_perm, source="poro")
Using a named function is often preferable when the transformation becomes more complex or needs to be reused.
Built-In Grid Attributes¶
The following built-in attribute names can be used as data sources in
GridProperty.apply().
These attributes are computed directly from the grid geometry and do not
require explicit property creation.
"x": X-coordinate of cell centers"y": Y-coordinate of cell centers"depth"(or"z"): Cell-center depth (positive downward)"top": Depth of the top face of each cell"bottom": Depth of the bottom face of each cell"thickness": Cell thickness (bottom - top)"active": Activity indicator (1 for active cells, 0 for inactive)
These attributes can be used directly as input sources. For example, permeability can be defined as a function of depth:
permeability.apply(
lambda depth: depth / 1000,
source="depth",
)
Important
These names are reserved and cannot be used when defining new properties. Attempting to create a property with one of these names will raise an error.
Using Multiple Sources¶
The apply() method can combine multiple sources, including both
properties and grid-derived attributes.
permeability.apply(
lambda poro, depth: 100*poro*thickness + depth,
source=(porosity, "depth", 'thickness'),
)
Property Statistics¶
Petres provides convenient access to common summary statistics.
print("Min Value:", permeability.min)
print("Max Value:", permeability.max)
print("Mean Value:", permeability.mean)
print("Median Value:", permeability.median)
print("Standard Deviation:", permeability.std)
You can also print a full summary:
print(permeability.summary())
This is useful for quickly checking whether the modeled values fall within reasonable physical limits.
Filling with a Uniform Distribution¶
For stochastic modeling, a property can be filled from a uniform distribution.
porosity.fill_uniform(low=0.24, high=0.30)
This generates values uniformly distributed between the specified lower and upper bounds.
Filling with a Normal Distribution¶
A normal distribution can be used when a property is expected to vary around a representative mean value.
porosity.fill_normal(mean=0.24, std=0.03)
Minimum and maximum clipping bounds may optionally be supplied:
porosity.fill_normal(mean=0.24, std=0.03, min=0.20, max=0.30)
To ensure reproducibility, a random seed may also be set:
porosity.fill_normal(mean=0.24, std=0.03, seed=42)
Filling with a Log-Normal Distribution¶
A log-normal distribution is often more suitable for positively skewed properties, especially for quantities such as permeability.
porosity.fill_lognormal(mean=0.24, std=0.03)
As with normal filling, optional bounds may be provided:
porosity.fill_lognormal(mean=0.24, std=0.03, min=0.20, max=0.30)
A seed can also be specified for reproducibility:
porosity.fill_lognormal(mean=0.24, std=0.03, seed=42)
Populating a Property from a NumPy Array¶
If you already have a precomputed 3D array of values, it can be assigned directly to the property.
import numpy as np
array = np.full(grid.shape, 0.24)
porosity.from_array(array)
This is especially useful when the property values come from an external workflow, a simulator output, or a separate numerical calculation.
Zone-Based Property Assignment¶
If the grid contains zone information, property values can be modeled separately for each zone. This is particularly useful when different formations require different property ranges or modeling assumptions. The example below assumes that the zones have already been defined, as shown in earlier tutorials (see Zone Modeling, Grid Modeling from Horizons and Zones).
porosity.fill(0.20, zone="Caprock")
porosity.fill(0.50, zone="Base")
The same zone-based idea can be used with other population methods as well`. For example:
porosity.fill_normal(mean=0.08, std=0.01, zone="Caprock")
porosity.fill_normal(mean=0.24, std=0.03, zone="Base")
This allows each zone to be modeled independently.
Interpolating Property Values from Wells¶
Property values can also be assigned by interpolating measurements taken at well locations. This is useful when property data are available only at sparse sample points and need to be distributed throughout the grid.
Defining Property Samples on Wells¶
There are two supported ways to assign property measurements to wells.
1. Depth-Dependent Samples¶
Use this approach when the property varies along the well trajectory. Each sample is associated with a specific depth:
well1.add_sample(name="porosity", value=100, depth=10)
well1.add_sample(name="porosity", value=120, depth=20)
well2.add_sample(name="porosity", value=50, depth=15)
well2.add_sample(name="porosity", value=70, depth=25)
Each (property, depth) pair must be unique per well. In other words, you cannot assign multiple values to the same property at the same depth within a well.
This mode enables 3D interpolation, where the property varies in both horizontal and vertical directions.
2. Single Sample per Well¶
Use this approach when only one representative value per well is available:
well1.add_sample(name="porosity", value=100)
well2.add_sample(name="porosity", value=50)
In this case, the value represents the entire well, and no depth variation is assumed. Interpolation will therefore be independent of depth.
Important
You cannot mix sampling modes for the same property on a single well — choose either depth-dependent or single sample method.
Mixing both modes for the same property is not allowed.
When performing interpolation, all wells must use the same sampling mode for the selected property.
Interpolating to the Grid¶
Once samples are defined, you can interpolate them onto the grid:
from petres.interpolators import IDWInterpolator
porosity.from_wells(
wells=[well1, well2],
interpolator=IDWInterpolator(),
)
If the input samples are depth-dependent, interpolation is performed in 3D. For single value samples, interpolation is performed in 2D and applied uniformly along the vertical axis.
Note
In this example, Inverse Distance Weighting
(InverseDistanceWeightingInterpolator) is used.
For more details and additional interpolation options, see Interpolators.
Filling Remaining Missing Values¶
In some workflows, such as zone-based modeling or interpolation, certain cells may remain unassigned and contain NaN values.
These values can be replaced using fill_nan():
porosity.fill_nan(0.0)
This is particularly useful for handling inactive cells and ensuring that all grid cells have valid values before further processing or export.
Complete Example¶
The following example combines several of the ideas introduced above.
from petres.grids import CornerPointGrid
import numpy as np
grid = CornerPointGrid.from_regular(
xlim=(0, 1000),
ylim=(0, 1000),
zlim=(0, 100),
ni=20,
nj=20,
nk=3,
)
porosity = grid.properties.create(
name="poro",
eclipse_keyword="PORO",
description="Porosity",
)
porosity.fill_normal(mean=0.24, std=0.03, min=0.20, max=0.30, seed=42)
permeability = grid.properties.create(
name="perm",
eclipse_keyword="PERM",
description="Permeability",
)
permeability.apply(
lambda poro, z: 100 * poro**3 + z,
source=(porosity, "z"),
)
print(porosity.summary())
print(permeability.summary())
porosity.show(cmap="viridis")
permeability.show(cmap="plasma")
Where to Go Next¶
Now that you know how to define and populate cell properties, the next step is usually to export the grid and its associated properties for simulation use (see Exporting Grid Models)