Source code for pycsamt.inversion.model

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Model containers used by :mod:`pycsamt.inversion`."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import numpy as np

from ..api.property import MetadataMixin, PyCSAMTObject
from .doc import _inversion_param_docs

__all__ = ["StartingModel", "ReferenceModel"]


[docs] @dataclass class StartingModel(PyCSAMTObject, MetadataMixin): resistivities: Any thicknesses: Any name: str = "" metadata: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: self.resistivities = np.asarray(self.resistivities, dtype=float) self.thicknesses = np.asarray(self.thicknesses, dtype=float) self.validate()
[docs] @classmethod def default(cls, n_layers: int = 4) -> StartingModel: """Return a conservative layered starting model. Parameters ---------- n_layers : int, default 4 Number of layers, including the bottom halfspace. Returns ------- StartingModel Default model with 100 ohm-m layers and geometrically increasing thicknesses between 100 m and 2000 m. Raises ------ ValueError If ``n_layers < 2``. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> start = StartingModel.default(n_layers=3) >>> start.resistivities.tolist() [100.0, 100.0, 100.0] >>> start.thicknesses.size 2 """ n_layers = int(n_layers) if n_layers < 2: raise ValueError("n_layers must be >= 2.") resistivities = np.full(n_layers, 100.0, dtype=float) thicknesses = np.geomspace(100.0, 2000.0, n_layers - 1) return cls(resistivities, thicknesses, name="default")
[docs] @classmethod def from_dict(cls, data: dict[str, Any]) -> StartingModel: """Build from singular or plural mapping keys. Parameters ---------- data : mapping Model mapping. Accepted keys are ``resistivity`` or ``resistivities`` for layer resistivity, ``thickness`` or ``thicknesses`` for layer thickness, plus optional ``name`` and ``metadata``. Returns ------- StartingModel Validated layered-earth model. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> start = StartingModel.from_dict({ ... "resistivity": [100.0, 500.0], ... "thickness": [250.0], ... "name": "two_layer", ... }) >>> start.name 'two_layer' """ return cls( data.get("resistivity", data.get("resistivities")), data.get("thickness", data.get("thicknesses")), name=str(data.get("name", "")), metadata=dict(data.get("metadata", {})), )
[docs] @classmethod def coerce(cls, value: Any, *, n_layers: int = 4) -> StartingModel: """Return *value* as a :class:`StartingModel`. Parameters ---------- value : StartingModel, mapping, object, or None Input model. Existing ``StartingModel`` instances are returned unchanged. Mappings are read by :meth:`from_dict`. Generic objects may expose ``resistivity``/``resistivities`` and ``thickness``/``thicknesses`` attributes. n_layers : int, default 4 Number of layers used only when *value* is ``None`` and a default model must be created. Returns ------- StartingModel Validated layered-earth model. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> StartingModel.coerce(None, n_layers=2).n_layers 2 >>> StartingModel.coerce({ ... "resistivities": [80.0, 250.0], ... "thicknesses": [500.0], ... }).depths.tolist() [0.0, 500.0] """ if value is None: return cls.default(n_layers=n_layers) if isinstance(value, cls): return value if isinstance(value, dict): return cls.from_dict(value) resistivity = getattr(value, "resistivity", None) if resistivity is None: resistivity = getattr(value, "resistivities", None) thickness = getattr(value, "thickness", None) if thickness is None: thickness = getattr(value, "thicknesses", None) return cls(resistivity, thickness, name=getattr(value, "name", ""))
[docs] @property def n_layers(self) -> int: return int(self.resistivities.size)
[docs] @property def depths(self) -> np.ndarray: """Top-of-layer depths in metres. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> StartingModel([100.0, 300.0, 800.0], [50.0, 150.0]).depths.tolist() [0.0, 50.0, 200.0] """ return np.r_[0.0, np.cumsum(self.thicknesses)]
[docs] def to_layered_model(self): """Return the existing :class:`pycsamt.forward.LayeredModel`. This adapter lets inversion starting/recovered models reuse the forward modelling API without duplicating layer containers. Returns ------- pycsamt.forward.LayeredModel Forward-model-compatible layered earth. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> layered = StartingModel([100.0, 300.0], [500.0]).to_layered_model() >>> layered.n_layers 2 """ from ..forward import LayeredModel return LayeredModel( resistivity=self.resistivities.copy(), thickness=self.thicknesses.copy(), name=self.name, )
[docs] def validate(self) -> None: """Validate layer shape and positivity. Raises ------ ValueError If resistivities or thicknesses are not 1-D, if fewer than two resistivity layers are supplied, if thickness count does not equal ``len(resistivities) - 1``, or if any value is non-positive. Examples -------- >>> from pycsamt.inversion.model import StartingModel >>> StartingModel([100.0, 300.0], [500.0]).validate() is None True """ if self.resistivities.ndim != 1 or self.thicknesses.ndim != 1: raise ValueError("resistivities and thicknesses must be 1-D.") if self.resistivities.size < 2: raise ValueError("at least two layers are required.") if self.thicknesses.size != self.resistivities.size - 1: raise ValueError("len(thicknesses) must be len(resistivities)-1.") if np.any(self.resistivities <= 0): raise ValueError("resistivities must be strictly positive.") if np.any(self.thicknesses <= 0): raise ValueError("thicknesses must be strictly positive.")
ReferenceModel = StartingModel StartingModel.__doc__ = f""" Starting or recovered layered-earth model. Resistivities are linear ohm-m values. The final layer is a halfspace, so ``len(thicknesses)`` must equal ``len(resistivities) - 1``. ``ReferenceModel`` is currently an alias of ``StartingModel``. Use it when the same layer container is being supplied as a regularization reference rather than as an optimizer starting point. Parameters ---------- {_inversion_param_docs.model.resistivities} {_inversion_param_docs.model.thicknesses} {_inversion_param_docs.model.name} {_inversion_param_docs.model.metadata} Notes ----- The model is deliberately small and backend-neutral. Built-in solvers optimize ``log10(resistivity)`` and ``log10(thickness)`` internally, but this public container stores physical linear values in ohm metres and metres. {_inversion_param_docs.model.examples} {_inversion_param_docs.model.references} """