Source code for pycsamt.inversion.config

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Configuration for physics-based EM inversion workflows."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from ..api.property import MetadataMixin, PyCSAMTObject
from ..models.config_io import (
    ConfigParameter,
    read_config_file,
    write_config_template,
)
from .doc import _inversion_examples, _inversion_param_docs

__all__ = ["InversionConfig"]


_SCHEMA = [
    ConfigParameter(
        "method",
        "EM method to invert. Accepted values are 'mt', 'amt', 'csamt', "
        "'emap', and 'tdem'. The built-in runnable inversion targets "
        "MT/AMT/CSAMT impedance rho/phase data and TDEM decay data.",
        "Survey",
    ),
    ConfigParameter(
        "dimension",
        "Inversion dimensionality: '1d', '2d', or '3d'. Built-in paths "
        "cover layered 1-D and stitched station-by-station 2-D sections; "
        "external adapters cover Occam2D and ModEM workflows.",
        "Survey",
    ),
    ConfigParameter(
        "backend",
        "Backend name: 'builtin', 'occam2d', 'modem', 'simpeg', or "
        "'pygimli'. Optional backends are imported only when selected.",
        "Backend",
    ),
    ConfigParameter(
        "data",
        "Input data object, mapping, or path. Mappings can contain freqs, "
        "rho_a, phase, times, values, errors, station_names, and station_x.",
        "Survey",
    ),
    ConfigParameter(
        "workdir",
        "Working directory for generated files, logs, and backend outputs.",
        "Output",
    ),
    ConfigParameter(
        "output_dir",
        "Optional output directory for exported products. Defaults to workdir.",
        "Output",
    ),
    ConfigParameter(
        "n_layers",
        "Number of 1-D layers, including the bottom halfspace.",
        "Model",
    ),
    ConfigParameter(
        "starting_model",
        "Starting model mapping with resistivities and thicknesses. If null, "
        "a conservative default layered model is generated.",
        "Model",
    ),
    ConfigParameter(
        "reference_model",
        "Optional reference model used by backends that support reference "
        "regularization.",
        "Model",
    ),
    ConfigParameter(
        "error_floor",
        "Relative error floor used when explicit errors are unavailable.",
        "Data Errors",
    ),
    ConfigParameter(
        "include_phase",
        "Include phase observations in the built-in MT/AMT objective.",
        "Data Errors",
    ),
    ConfigParameter(
        "phase_error",
        "Absolute phase error in degrees used by the built-in objective.",
        "Data Errors",
    ),
    ConfigParameter(
        "regularization",
        "Regularization kind: 'none', 'smooth', 'damped', or 'blocky'.",
        "Regularization",
    ),
    ConfigParameter(
        "max_iter",
        "Maximum optimizer iterations for runnable local backends.",
        "Solver",
    ),
    ConfigParameter(
        "tol",
        "Optimizer termination tolerance.",
        "Solver",
    ),
    ConfigParameter(
        "bounds",
        "Optional parameter bounds. For the built-in 1-D backend use keys "
        "'log10_rho' and 'log10_thickness'.",
        "Solver",
    ),
    ConfigParameter(
        "run_external",
        "Whether external-code backends may launch their executable. Phase 1 "
        "defaults to preparation/loading only.",
        "Backend",
    ),
    ConfigParameter(
        "backend_options",
        "Backend-specific keyword options passed through unchanged.",
        "Backend",
    ),
    ConfigParameter(
        "metadata",
        "Free-form provenance metadata attached to results.",
        "Output",
    ),
]


[docs] @dataclass class InversionConfig(PyCSAMTObject, MetadataMixin): """Source-of-truth configuration for :mod:`pycsamt.inversion`.""" method: str = "mt" dimension: str = "1d" backend: str = "builtin" data: Any = None workdir: str = "pycsamt_inversion" output_dir: str | None = None n_layers: int = 4 starting_model: Any = None reference_model: Any = None error_floor: float = 0.05 include_phase: bool = True phase_error: float = 3.0 regularization: str = "smooth" max_iter: int = 80 tol: float = 1e-5 bounds: dict[str, Any] = field(default_factory=dict) run_external: bool = False backend_options: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: self.method = str(self.method).lower() self.dimension = str(self.dimension).lower() self.backend = str(self.backend).lower() self.workdir = str(self.workdir) if self.output_dir is not None: self.output_dir = str(self.output_dir) self.n_layers = int(self.n_layers) self.error_floor = float(self.error_floor) self.include_phase = bool(self.include_phase) self.phase_error = float(self.phase_error) self.max_iter = int(self.max_iter) self.tol = float(self.tol) self.run_external = bool(self.run_external) self.bounds = dict(self.bounds or {}) self.backend_options = dict(self.backend_options or {}) self.metadata = dict(self.metadata or {}) self.validate()
[docs] @property def output_path(self) -> Path: """Resolved output directory.""" return Path(self.output_dir or self.workdir)
[docs] def validate(self) -> None: if self.method not in {"mt", "amt", "csamt", "emap", "tdem"}: raise ValueError("method must be mt/amt/csamt/emap/tdem.") if self.dimension not in {"1d", "2d", "3d"}: raise ValueError("dimension must be '1d', '2d', or '3d'.") if self.backend not in { "builtin", "occam2d", "modem", "simpeg", "pygimli", }: raise ValueError("unsupported inversion backend.") if self.n_layers < 2: raise ValueError("n_layers must be >= 2.") if self.error_floor < 0: raise ValueError("error_floor must be non-negative.") if self.phase_error <= 0: raise ValueError("phase_error must be positive.") if self.max_iter <= 0: raise ValueError("max_iter must be positive.") if self.tol <= 0: raise ValueError("tol must be positive.")
[docs] def to_backend(self): """Instantiate the selected backend.""" from .backends import get_backend return get_backend(self.backend)(self)
[docs] def write_template( self, path: str | Path, *, fmt: str | None = None, ) -> Path: """Write an editable configuration template.""" return write_config_template( path, self, _SCHEMA, fmt=fmt, title="PyCSAMT physics-based EM inversion configuration", )
[docs] @classmethod def from_file( cls, path: str | Path, *, strict: bool = True ) -> InversionConfig: """Load a Python/JSON/YAML configuration file.""" return cls(**read_config_file(path, cls, strict=strict))
read = from_file
InversionConfig.__doc__ = f""" Source-of-truth configuration for :mod:`pycsamt.inversion`. Parameters ---------- {_inversion_param_docs.common.method} {_inversion_param_docs.common.dimension} {_inversion_param_docs.common.backend} {_inversion_param_docs.common.data} {_inversion_param_docs.common.workdir} {_inversion_param_docs.model.n_layers} {_inversion_param_docs.model.starting_model} {_inversion_param_docs.model.reference_model} {_inversion_param_docs.errors.error_floor} {_inversion_param_docs.errors.include_phase} {_inversion_param_docs.errors.phase_error} {_inversion_param_docs.model.regularization} {_inversion_param_docs.solver.max_iter} {_inversion_param_docs.solver.tol} {_inversion_param_docs.solver.bounds} {_inversion_param_docs.solver.run_external} {_inversion_param_docs.common.backend_options} {_inversion_examples.mt1d} """