pycsamt.ai.geology.lenses#

Rotated ellipsoidal lenses embedded in layered electrical geology.

Lens geometry is evaluated at cell centres. A lens may have a sharp boundary or a finite transition shell blended in log10-resistivity space. Overlap is never implicit: callers choose a conflict policy and the resulting overlap count is retained for auditing.

Functions

insert_lenses(base, lenses, *[, conflict_policy])

Insert electrical lenses into a layered model with explicit overlap rules.

Classes

EllipsoidalLens(name, center_x_m, ...[, ...])

Define a rotated 2-D elliptical or 3-D ellipsoidal electrical body.

LensGeology(base, lenses, resistivity_ohm_m, ...)

Immutable layered model after one or more lens insertions.

class pycsamt.ai.geology.lenses.EllipsoidalLens(name, center_x_m, center_z_m, radius_x_m, radius_z_m, resistivity_ohm_m, center_y_m=None, radius_y_m=None, azimuth_deg=0.0, dip_deg=0.0, transition_fraction=0.0)[source]

Bases: object

Define a rotated 2-D elliptical or 3-D ellipsoidal electrical body.

Parameters:
  • name (str) – Unique non-empty body name.

  • center_x_m (float) – Horizontal and depth coordinates of the body centre in metres.

  • center_z_m (float) – Horizontal and depth coordinates of the body centre in metres.

  • radius_x_m (float) – Positive principal semi-axis lengths in metres.

  • radius_z_m (float) – Positive principal semi-axis lengths in metres.

  • resistivity_ohm_m (float) – Positive target resistivity at the body core.

  • center_y_m (float or None, optional) – Second horizontal centre and radius. Both are required on a 3-D grid and both must be omitted on a 2-D grid.

  • radius_y_m (float or None, optional) – Second horizontal centre and radius. Both are required on a 3-D grid and both must be omitted on a 2-D grid.

  • azimuth_deg (float, default=0.0) – Clockwise rotation of the horizontal x principal axis in 3-D.

  • dip_deg (float, default=0.0) – Downward rotation of the x principal axis toward increasing depth. It rotates the x-z ellipse in 2-D and the azimuthal major-z plane in 3-D.

  • transition_fraction (float, default=0.0) – Fraction of the normalized outer radius occupied by a smooth transition shell. It must lie in [0, 1). Zero gives a sharp boundary.

Examples

A dipping conductive lens in a 2-D section:

>>> lens = EllipsoidalLens(
...     "conductor",
...     center_x_m=1000,
...     center_z_m=400,
...     radius_x_m=500,
...     radius_z_m=100,
...     resistivity_ohm_m=5,
...     dip_deg=20,
... )
>>> lens.dimension
2

A 3-D ellipsoid additionally declares y geometry:

>>> body = EllipsoidalLens(
...     "body", 0, 300, 400, 100, 10, center_y_m=0, radius_y_m=200
... )
>>> body.dimension
3
name: str
center_x_m: float
center_z_m: float
radius_x_m: float
radius_z_m: float
resistivity_ohm_m: float
center_y_m: float | None = None
radius_y_m: float | None = None
azimuth_deg: float = 0.0
dip_deg: float = 0.0
transition_fraction: float = 0.0
property dimension: int[source]

Return the dimensionality implied by the lens geometry.

Returns:

Two when y geometry is absent, otherwise three.

Return type:

{2, 3}

Examples

>>> EllipsoidalLens("a", 0, 1, 2, 1, 10).dimension
2
validate_grid(grid, *, require_intersection=True)[source]

Validate dimensional and spatial compatibility with a grid.

Parameters:
  • grid (GeologyGrid) – Candidate 2-D section or 3-D volume.

  • require_intersection (bool, default=True) – Require at least one cell centre inside the body.

Returns:

Successful return means the lens can be rasterized.

Return type:

None

Raises:

ValueError – If dimensions differ or no cell centre intersects the lens.

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> lens = EllipsoidalLens("a", 200, 100, 150, 75, 10)
>>> lens.validate_grid(grid) is None
True
normalized_radius(grid)[source]

Evaluate dimensionless ellipsoidal radius at every cell centre.

Parameters:

grid (GeologyGrid) – Grid with the same dimensionality as the lens.

Returns:

Array shaped like grid. Values at or below one are inside the lens; zero is the geometric centre.

Return type:

ndarray

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> lens = EllipsoidalLens("a", 200, 100, 150, 75, 10)
>>> lens.normalized_radius(grid).shape
(4, 4)
blend_weight(grid)[source]

Return the lens contribution weight at every cell centre.

Parameters:

grid (GeologyGrid) – Compatible geological grid.

Returns:

Values in [0, 1]. A sharp lens is one inside and zero outside; a transition shell uses cubic smoothstep interpolation.

Return type:

ndarray

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> lens = EllipsoidalLens(
...     "a", 200, 100, 150, 75, 10, transition_fraction=0.2
... )
>>> np.all(
...     (lens.blend_weight(grid) >= 0) & (lens.blend_weight(grid) <= 1)
... )
True
to_dict()[source]

Return a JSON-serializable lens definition.

Returns:

Versioned geometry, orientation, electrical value, and transition.

Return type:

dict

Examples

>>> EllipsoidalLens("a", 0, 1, 2, 1, 10).to_dict()["name"]
'a'
classmethod from_dict(data)[source]

Restore a validated lens definition.

Parameters:

data (mapping) – State returned by to_dict().

Returns:

Immutable body definition.

Return type:

EllipsoidalLens

Examples

>>> lens = EllipsoidalLens("a", 0, 1, 2, 1, 10)
>>> EllipsoidalLens.from_dict(lens.to_dict()) == lens
True
class pycsamt.ai.geology.lenses.LensGeology(base, lenses, resistivity_ohm_m, lens_index, overlap_count, conflict_policy='error')[source]

Bases: object

Immutable layered model after one or more lens insertions.

Parameters:
  • base (LayeredGeology) – Self-contained stratigraphic model before lens insertion.

  • lenses (sequence of EllipsoidalLens) – Bodies in declared precedence order.

  • resistivity_ohm_m (ndarray) – Final positive resistivity model shaped like base.grid.

  • lens_index (ndarray of int) – Assigned body index per cell, or -1 outside assigned bodies.

  • overlap_count (ndarray of int) – Number of geometric lens envelopes covering each cell.

  • conflict_policy ({"error", "first", "last", "most_conductive", "most_resistive"}) – Policy used where body envelopes overlap.

Examples

>>> grid = GeologyGrid.regular_2d(nx=8, nz=6, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 400, 150, 200, 75, 10)]
... )
>>> model.resistivity_ohm_m.shape
(6, 8)
base: LayeredGeology
lenses: tuple[EllipsoidalLens, ...]
resistivity_ohm_m: ndarray
lens_index: ndarray
overlap_count: ndarray
conflict_policy: str = 'error'
property model_hash: str[source]

Return a digest of base, lenses, final values, and overlap policy.

Returns:

Platform-stable SHA-256 digest.

Return type:

str

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> len(model.model_hash)
64
lens_mask(lens)[source]

Return cells assigned to one lens after conflict resolution.

Parameters:

lens (int or str) – Zero-based lens index or exact body name.

Returns:

Read-only assigned-cell mask.

Return type:

ndarray of bool

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> model.lens_mask("lens").any()
True
summary()[source]

Return JSON-compatible lens occupancy and resistivity diagnostics.

Returns:

Assigned fractions, overlap fraction, resistivity range, and hashes.

Return type:

dict

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> model.summary()["n_lenses"]
1
provenance()[source]

Return generation provenance without final cell arrays.

Returns:

Base provenance/hash, lens definitions, and conflict policy.

Return type:

dict

Examples

>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> model.provenance()["conflict_policy"]
'error'
to_npz(path)[source]

Persist base geology and lenses in one pickle-free NPZ archive.

Parameters:

path (str or pathlib.Path) – Destination archive.

Returns:

Requested destination path.

Return type:

pathlib.Path

Examples

>>> from tempfile import TemporaryDirectory
>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> with TemporaryDirectory() as directory:
...     path = model.to_npz(Path(directory) / "lenses.npz")
...     restored = LensGeology.from_npz(path)
>>> restored.model_hash == model.model_hash
True
classmethod from_npz(path)[source]

Load and validate a self-contained lens-geology archive.

Parameters:

path (str or pathlib.Path) – Archive written by to_npz().

Returns:

Immutable restored base and body model.

Return type:

LensGeology

Examples

>>> from tempfile import TemporaryDirectory
>>> grid = GeologyGrid.regular_2d(nx=4, nz=4, dx_m=100, dz_m=50)
>>> base = __import__(
...     "pycsamt.ai.geology", fromlist=[""]
... ).generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> model = insert_lenses(
...     base, [EllipsoidalLens("lens", 200, 100, 100, 50, 10)]
... )
>>> with TemporaryDirectory() as directory:
...     restored = LensGeology.from_npz(
...         model.to_npz(Path(directory) / "m.npz")
...     )
>>> np.array_equal(restored.resistivity_ohm_m, model.resistivity_ohm_m)
True
pycsamt.ai.geology.lenses.insert_lenses(base, lenses, *, conflict_policy='error')[source]

Insert electrical lenses into a layered model with explicit overlap rules.

Parameters:
  • base (LayeredGeology) – Stratigraphic resistivity model to modify immutably.

  • lenses (sequence of EllipsoidalLens) – One or more uniquely named compatible bodies.

  • conflict_policy ({"error", "first", "last", "most_conductive", "most_resistive"}, default="error") – error rejects geometric overlap. first or last gives precedence by declaration order. The remaining policies select the lowest or highest blended cell resistivity.

Returns:

New immutable model; base remains unchanged.

Return type:

LensGeology

Raises:

ValueError – If bodies are empty, incompatible, duplicate-named, non-intersecting, or overlap under the error policy.

Examples

>>> from pycsamt.ai.geology import generate_layered_geology
>>> grid = GeologyGrid.regular_2d(nx=12, nz=8, dx_m=100, dz_m=50)
>>> base = generate_layered_geology(
...     grid, [ElectricalLayer("earth", 100)], [], seed=0
... )
>>> lens = EllipsoidalLens(
...     "conductor", 600, 200, 250, 100, 5, transition_fraction=0.2
... )
>>> result = insert_lenses(base, [lens])
>>> np.min(result.resistivity_ohm_m) < 100
True
>>> np.array_equal(base.resistivity_ohm_m, np.full(grid.shape, 100.0))
True