# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""SimPEG backend for physics-based EM inversion.
SimPEG is optional and imported only when this backend is selected. The
backend wraps SimPEG's natural-source electromagnetic API for MT, AMT, and
CSAMT inversion. It supports 1-D soundings, stitched station-by-station 2-D
profiles, and a 3-D primary-secondary natural-source path when the installed
SimPEG version exposes the required classes.
The model parameter is log conductivity on SimPEG mesh cells. PyCSAMT converts
user-facing layered resistivity starting models into log-conductivity cells,
runs the inversion, and converts recovered models back to resistivity-oriented
``InversionResult`` payloads.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import numpy as np
from ..base import BaseInversionBackend
from ..data import EMData
from ..mesh import (
InversionMesh,
build_1d_tensor_mesh,
build_3d_tensor_mesh,
)
from ..model import StartingModel
from ..objective import (
component_errors,
component_mask,
weighted_rms,
)
from ..regularization import regularization_from_config
from ..results import InversionResult
__all__ = ["SimPEGBackend"]
@dataclass
class _SimPEGModules:
discretize: Any
maps: Any
data: Any
data_misfit: Any
directives: Any
inverse_problem: Any
inversion: Any
optimization: Any
regularization: Any
nsem: Any
[docs]
class SimPEGBackend(BaseInversionBackend):
name = "simpeg"
supports = (
("mt", "1d"),
("mt", "2d"),
("mt", "3d"),
("amt", "1d"),
("amt", "2d"),
("amt", "3d"),
("csamt", "1d"),
("csamt", "2d"),
("csamt", "3d"),
)
[docs]
def run(self, data: Any | None = None) -> InversionResult:
"""Run a SimPEG natural-source EM inversion.
Parameters
----------
data : mapping, object, sequence, or path-like, optional
Optional data override for this call. When omitted, the backend
uses ``self.config.data``. Values are coerced through
:class:`pycsamt.inversion.data.EMData`.
Returns
-------
InversionResult
Backend-neutral result. For 1-D runs, ``model`` is a recovered
:class:`pycsamt.inversion.model.StartingModel`. For stitched 2-D
runs, ``model`` is a dictionary containing ``rho_2d`` and profile
coordinates. For 3-D runs, ``model`` contains ``rho_3d`` and
``x_centers``, ``y_centers``, and ``z_centers`` arrays.
Raises
------
ImportError
If SimPEG or discretize is not installed.
ValueError
If data do not contain frequencies plus apparent resistivity
and/or phase.
NotImplementedError
If the configured method/dimension pair is unsupported by this
backend or by the installed SimPEG natural-source API.
Examples
--------
>>> from pycsamt.inversion.backends.simpeg import SimPEGBackend
>>> from pycsamt.inversion.config import InversionConfig
>>> cfg = InversionConfig(
... method="mt",
... dimension="1d",
... backend="simpeg",
... data={"freqs": [1.0, 10.0],
... "rho_a": [100.0, 120.0],
... "phase": [45.0, 47.0]},
... max_iter=8,
... )
>>> result = SimPEGBackend(cfg).run() # doctest: +SKIP
"""
self.check_supported()
modules = _load_simpeg()
em_data = self.prepare_data(data)
if not em_data.has_mt_response:
raise ValueError(
"SimPEG natural-source backend requires frequencies plus "
"rho_a and/or phase."
)
if self.config.dimension == "3d":
return self._run_3d(em_data, modules)
if self.config.dimension == "2d":
return self._run_profile(em_data, modules)
return self._run_sounding(em_data, modules, station_index=None)
def _run_3d(
self, em_data: EMData, modules: _SimPEGModules
) -> InversionResult:
cfg = self.config
mesh, centers = _build_3d_mesh(em_data, cfg.backend_options, modules)
survey = _build_nsem_survey(em_data, modules, dimension="3d")
observed, errors = _pack_nsem_observations(em_data, cfg)
active_map = modules.maps.IdentityMap(nP=mesh.nC)
sigma_map = modules.maps.ExpMap(mesh) * active_map
sigma_primary = float(
cfg.backend_options.get("sigma_primary", 1.0 / 100.0)
)
simulation = modules.nsem.Simulation3DPrimarySecondary(
mesh,
survey=survey,
sigmaMap=sigma_map,
sigmaPrimary=np.full(mesh.nC, sigma_primary, dtype=float),
)
simpeg_data = _build_simpeg_data(survey, observed, errors, modules)
data_misfit = modules.data_misfit.L2DataMisfit(
data=simpeg_data,
simulation=simulation,
)
reg = _build_regularization(mesh, active_map, modules, cfg)
opt = modules.optimization.InexactGaussNewton(
maxIter=cfg.max_iter,
maxIterLS=int(cfg.backend_options.get("max_iter_ls", 20)),
tolX=cfg.tol,
tolF=cfg.tol,
)
inv_problem = modules.inverse_problem.BaseInvProblem(
data_misfit, reg, opt
)
beta0 = float(cfg.backend_options.get("beta0", 1.0))
inv_problem.beta = beta0
inv = modules.inversion.BaseInversion(
inv_problem,
directiveList=_build_directives(modules, cfg),
)
start = StartingModel.coerce(
cfg.starting_model, n_layers=cfg.n_layers
)
m0 = _starting_3d_log_sigma(start, centers)
recovered_model = inv.run(m0)
predicted = np.asarray(simulation.dpred(recovered_model), dtype=float)
rms = weighted_rms(observed, predicted, errors)
rho_3d = _rho_3d_from_log_sigma(recovered_model, mesh)
mesh_out = InversionMesh(
dimension="3d",
x_centers=centers["x"],
z_centers=centers["z_depth"],
native=mesh,
metadata={
"engine": "simpeg",
"y_centers": centers["y"].tolist(),
"mesh_shape": tuple(int(v) for v in _mesh_shape(mesh)),
},
)
return InversionResult(
method=cfg.method,
dimension="3d",
backend=self.name,
status="success",
model={
"rho_3d": rho_3d,
"x_centers": centers["x"],
"y_centers": centers["y"],
"z_centers": centers["z_depth"],
"station_x": _station_x(em_data, em_data.n_stations),
"station_y": _station_y(em_data, em_data.n_stations),
"station_names": _station_names(em_data, em_data.n_stations),
},
mesh=mesh_out,
data=em_data,
predicted=predicted,
rms=rms,
objective=float(data_misfit(recovered_model)),
n_iter=int(getattr(opt, "iter", 0)),
workdir=cfg.workdir,
native={
"mesh": mesh,
"survey": survey,
"data": simpeg_data,
"simulation": simulation,
"data_misfit": data_misfit,
"regularization": reg,
"optimization": opt,
"inverse_problem": inv_problem,
"inversion": inv,
"recovered_model": recovered_model,
},
metadata={
**cfg.metadata,
"engine": "simpeg",
"simulation": "Simulation3DPrimarySecondary",
"source": "PlanewaveXYPrimary",
"model_parameter": "log_sigma_cells",
"beta0": beta0,
"sigma_primary": sigma_primary,
},
)
def _run_profile(
self, em_data: EMData, modules: _SimPEGModules
) -> InversionResult:
cfg = self.config
n_st = em_data.n_stations
names = _station_names(em_data, n_st)
xs = _station_x(em_data, n_st)
columns: list[np.ndarray] = []
station_results: list[InversionResult] = []
used: list[int] = []
warnings: list[str] = []
z_centers = None
for idx in range(n_st):
sounding = _station_data(em_data, idx)
try:
result = self._run_sounding(
sounding, modules, station_index=idx
)
except Exception as exc:
warnings.append(
f"{names[idx]}: SimPEG inversion failed: {exc}"
)
continue
station_results.append(result)
used.append(idx)
columns.append(np.log10(result.model.resistivities))
if z_centers is None and result.mesh is not None:
z_centers = result.mesh.z_centers
if not columns:
raise RuntimeError("all SimPEG station inversions failed.")
rho_2d = np.stack(columns, axis=1)
used_x = xs[used]
used_names = [names[i] for i in used]
if z_centers is None:
z_centers = np.arange(rho_2d.shape[0], dtype=float)
mesh = InversionMesh(
dimension="2d",
x_centers=used_x,
z_centers=z_centers,
metadata={"engine": "simpeg", "profile_mode": "stitched_1d"},
)
rms_values = np.asarray([r.rms for r in station_results], dtype=float)
return InversionResult(
method=cfg.method,
dimension="2d",
backend=self.name,
status="success" if not warnings else "needs_review",
model={
"rho_2d": rho_2d,
"x_centers": used_x,
"z_centers": z_centers,
"station_x": used_x,
"station_names": used_names,
},
mesh=mesh,
data=em_data,
predicted=[r.predicted for r in station_results],
rms=float(np.nanmean(rms_values)),
objective=float(
np.nansum([r.objective for r in station_results])
),
n_iter=int(np.sum([r.n_iter for r in station_results])),
workdir=cfg.workdir,
native=station_results,
warnings=warnings,
metadata={
**cfg.metadata,
"engine": "simpeg",
"profile_mode": "stitched_station_1d",
"station_rms": rms_values.tolist(),
},
)
def _run_sounding(
self,
em_data: EMData,
modules: _SimPEGModules,
*,
station_index: int | None,
) -> InversionResult:
cfg = self.config
start = StartingModel.coerce(
cfg.starting_model, n_layers=cfg.n_layers
)
mesh, z_centers = _build_1d_mesh(start, cfg.backend_options, modules)
survey = _build_nsem_survey(em_data, modules)
observed, errors = _pack_nsem_observations(em_data, cfg)
active_map = modules.maps.IdentityMap(nP=mesh.nC)
sigma_map = modules.maps.ExpMap(mesh) * active_map
simulation = modules.nsem.Simulation1DElectricField(
mesh,
survey=survey,
sigmaMap=sigma_map,
)
simpeg_data = _build_simpeg_data(survey, observed, errors, modules)
data_misfit = modules.data_misfit.L2DataMisfit(
data=simpeg_data,
simulation=simulation,
)
reg = _build_regularization(mesh, active_map, modules, cfg)
opt = modules.optimization.InexactGaussNewton(
maxIter=cfg.max_iter,
maxIterLS=int(cfg.backend_options.get("max_iter_ls", 20)),
tolX=cfg.tol,
tolF=cfg.tol,
)
inv_problem = modules.inverse_problem.BaseInvProblem(
data_misfit, reg, opt
)
beta0 = float(cfg.backend_options.get("beta0", 1.0))
inv_problem.beta = beta0
directive_list = _build_directives(modules, cfg)
inv = modules.inversion.BaseInversion(
inv_problem, directiveList=directive_list
)
m0 = _starting_sigma_model(start, z_centers)
recovered_model = inv.run(m0)
predicted = np.asarray(simulation.dpred(recovered_model), dtype=float)
rms = weighted_rms(observed, predicted, errors)
recovered = _model_from_sigma_cells(
recovered_model, z_centers, start.n_layers
)
mesh_out = InversionMesh(
dimension="1d",
x_centers=np.array([0.0]),
z_centers=_layer_centers(recovered.thicknesses),
native=mesh,
metadata={
"engine": "simpeg",
"n_cells": int(mesh.nC),
"cell_z_centers": z_centers.tolist(),
},
)
return InversionResult(
method=cfg.method,
dimension="1d",
backend=self.name,
status="success",
model=recovered,
mesh=mesh_out,
data=em_data,
predicted=predicted,
rms=rms,
objective=float(data_misfit(recovered_model)),
n_iter=int(getattr(opt, "iter", 0)),
workdir=cfg.workdir,
native={
"mesh": mesh,
"survey": survey,
"data": simpeg_data,
"simulation": simulation,
"data_misfit": data_misfit,
"regularization": reg,
"optimization": opt,
"inverse_problem": inv_problem,
"inversion": inv,
"recovered_model": recovered_model,
},
metadata={
**cfg.metadata,
"engine": "simpeg",
"model_parameter": "log_sigma_cells",
"station_index": station_index,
"beta0": beta0,
},
)
def _load_simpeg() -> _SimPEGModules:
try:
import discretize
from simpeg import (
data,
data_misfit,
directives,
inverse_problem,
inversion,
maps,
optimization,
regularization,
)
from simpeg.electromagnetics import (
natural_source as nsem,
)
except ImportError as exc:
raise ImportError(
"SimPEG backend selected, but SimPEG/discretize is not installed. "
"Install SimPEG, or choose backend='builtin'/'occam2d'."
) from exc
return _SimPEGModules(
discretize=discretize,
maps=maps,
data=data,
data_misfit=data_misfit,
directives=directives,
inverse_problem=inverse_problem,
inversion=inversion,
optimization=optimization,
regularization=regularization,
nsem=nsem,
)
def _build_1d_mesh(
start: StartingModel,
options: dict[str, Any],
modules: _SimPEGModules,
):
return build_1d_tensor_mesh(start, options, modules.discretize.TensorMesh)
def _build_3d_mesh(
em_data: EMData,
options: dict[str, Any],
modules: _SimPEGModules,
):
station_x = _station_x(em_data, em_data.n_stations)
station_y = _station_y(em_data, em_data.n_stations)
return build_3d_tensor_mesh(
station_x,
station_y,
options,
modules.discretize.TensorMesh,
)
def _build_nsem_survey(
em_data: EMData,
modules: _SimPEGModules,
*,
dimension: str = "1d",
):
receivers = modules.nsem.receivers
sources = modules.nsem.sources
survey_cls = getattr(modules.nsem, "Survey", None)
if survey_cls is None:
from simpeg import survey as survey_mod
survey_cls = survey_mod.Survey
freqs = np.asarray(em_data.frequencies, dtype=float)
if dimension == "3d":
location = _station_locations(em_data)
else:
location = np.array([[0.0, 0.0, 0.0]], dtype=float)
source_list = []
for freq in freqs:
rx_list = []
if em_data.rho_a is not None:
rx_list.append(
receivers.PointNaturalSource(
location,
orientation="xy",
component="apparent_resistivity",
)
)
if em_data.phase is not None:
rx_list.append(
receivers.PointNaturalSource(
location,
orientation="xy",
component="phase",
)
)
if dimension == "3d" and hasattr(sources, "PlanewaveXYPrimary"):
source_list.append(
sources.PlanewaveXYPrimary(rx_list, frequency=float(freq))
)
else:
source_list.append(
sources.Planewave(rx_list, frequency=float(freq))
)
return survey_cls(source_list)
def _build_simpeg_data(
survey: Any,
observed: np.ndarray,
errors: np.ndarray,
modules: _SimPEGModules,
):
try:
return modules.data.Data(
survey,
dobs=observed,
standard_deviation=errors,
)
except TypeError:
return modules.nsem.survey.Data(
survey,
dobs=observed,
standard_deviation=errors,
)
def _pack_nsem_observations(
em_data: EMData,
cfg: Any,
) -> tuple[np.ndarray, np.ndarray]:
values: list[float] = []
errors: list[float] = []
rho = (
None
if em_data.rho_a is None
else np.asarray(em_data.rho_a, dtype=float)
)
phase = (
None
if em_data.phase is None
else np.asarray(em_data.phase, dtype=float)
)
raw_err = (
None
if em_data.errors is None
else np.asarray(em_data.errors, dtype=float)
)
n = em_data.n_samples
for i in range(n):
if rho is not None:
rho_i = rho[i] if rho.ndim == 1 else rho[:, i]
mask_i = component_mask(rho_i, cfg, component="rho").reshape(-1)
values.extend(np.asarray(rho_i, dtype=float).reshape(-1).tolist())
err_i = (
raw_err[i]
if raw_err is not None and raw_err.ndim == 1
else (raw_err[:, i] if raw_err is not None else None)
)
err = component_errors(
rho_i, cfg, component="rho", explicit=err_i
).reshape(-1)
err[~mask_i] = 1e30
errors.extend(err.tolist())
if phase is not None:
phase_i = phase[i] if phase.ndim == 1 else phase[:, i]
phase_i = np.asarray(phase_i, dtype=float).reshape(-1)
mask_i = component_mask(phase_i, cfg, component="phase").reshape(
-1
)
values.extend(phase_i.tolist())
err = component_errors(phase_i, cfg, component="phase").reshape(
-1
)
err[~mask_i] = 1e30
errors.extend(err.tolist())
return np.asarray(values, dtype=float), np.asarray(errors, dtype=float)
def _build_regularization(
mesh: Any,
mapping: Any,
modules: _SimPEGModules,
cfg: Any,
):
reg_cfg = regularization_from_config(cfg)
kind = reg_cfg.kind
if hasattr(modules.regularization, "WeightedLeastSquares"):
reg = modules.regularization.WeightedLeastSquares(
mesh, mapping=mapping
)
else:
reg = modules.regularization.Simple(mesh, mapping=mapping)
if kind == "none":
_set_if_present(reg, "alpha_s", 0.0)
_set_if_present(reg, "alpha_x", 0.0)
_set_if_present(reg, "alpha_y", 0.0)
_set_if_present(reg, "alpha_z", 0.0)
elif kind == "damped":
_set_if_present(reg, "alpha_s", reg_cfg.alpha_s)
_set_if_present(reg, "alpha_x", 0.0)
_set_if_present(reg, "alpha_y", 0.0)
_set_if_present(reg, "alpha_z", 0.0)
else:
_set_if_present(reg, "alpha_s", reg_cfg.alpha_s)
_set_if_present(reg, "alpha_x", reg_cfg.alpha_x)
_set_if_present(
reg,
"alpha_y",
float(cfg.backend_options.get("alpha_y", reg_cfg.alpha_x)),
)
_set_if_present(reg, "alpha_z", reg_cfg.alpha_z)
reference = _simpeg_reference_model(cfg, mesh)
if reference is not None:
_set_if_present(reg, "reference_model", reference)
_set_if_present(reg, "mref", reference)
return reg
def _set_if_present(obj: Any, name: str, value: Any) -> None:
if hasattr(obj, name):
setattr(obj, name, value)
def _simpeg_reference_model(cfg: Any, mesh: Any) -> np.ndarray | None:
value = cfg.backend_options.get("reference_model", cfg.reference_model)
if value is None:
return None
raw = getattr(value, "resistivities", value)
try:
arr = np.asarray(raw, dtype=float).reshape(-1)
except Exception:
return None
n_cells = int(getattr(mesh, "nC", arr.size))
if arr.size != n_cells:
return None
if np.all(arr > 0.0):
return np.log(1.0 / arr)
return arr
def _build_directives(modules: _SimPEGModules, cfg: Any) -> list[Any]:
directives = []
if bool(cfg.backend_options.get("estimate_beta", True)):
beta_cls = getattr(modules.directives, "BetaEstimate_ByEig", None)
if beta_cls is not None:
directives.append(
beta_cls(
beta0_ratio=float(
cfg.backend_options.get("beta0_ratio", 1.0)
)
)
)
target_cls = getattr(modules.directives, "TargetMisfit", None)
if target_cls is not None:
directives.append(
target_cls(
chifact=float(cfg.backend_options.get("target_chifact", 1.0))
)
)
beta_schedule = getattr(modules.directives, "BetaSchedule", None)
if beta_schedule is not None:
directives.append(
beta_schedule(
coolingFactor=float(
cfg.backend_options.get("cooling_factor", 2.0)
),
coolingRate=int(cfg.backend_options.get("cooling_rate", 1)),
)
)
return directives
def _starting_sigma_model(
start: StartingModel, z_centers: np.ndarray
) -> np.ndarray:
rho = np.asarray(start.resistivities, dtype=float)
depths = np.r_[0.0, np.cumsum(start.thicknesses)]
out = np.empty_like(z_centers, dtype=float)
for i, z in enumerate(z_centers):
layer = int(np.searchsorted(depths[1:], z, side="right"))
out[i] = np.log(1.0 / rho[min(layer, rho.size - 1)])
return out
def _starting_3d_log_sigma(
start: StartingModel,
centers: dict[str, np.ndarray],
) -> np.ndarray:
z_depth = np.asarray(centers["z_depth"], dtype=float)
one_d = _starting_sigma_model(start, z_depth)
nx = len(centers["x"])
ny = len(centers["y"])
nz = len(centers["z"])
model = np.empty((nx, ny, nz), dtype=float)
for iz in range(nz):
model[:, :, iz] = one_d[iz]
return model.reshape(-1, order="F")
def _rho_3d_from_log_sigma(log_sigma: np.ndarray, mesh: Any) -> np.ndarray:
shape = _mesh_shape(mesh)
sigma = np.exp(np.asarray(log_sigma, dtype=float)).reshape(
shape, order="F"
)
return 1.0 / np.maximum(sigma, 1e-12)
def _mesh_shape(mesh: Any) -> tuple[int, int, int]:
if hasattr(mesh, "shape_cells"):
shape = tuple(int(v) for v in mesh.shape_cells)
elif hasattr(mesh, "vnC"):
shape = tuple(int(v) for v in mesh.vnC)
else:
raise AttributeError("Cannot determine SimPEG mesh cell shape.")
if len(shape) != 3:
raise ValueError("Expected a 3-D mesh.")
return shape
def _model_from_sigma_cells(
log_sigma: np.ndarray,
z_centers: np.ndarray,
n_layers: int,
) -> StartingModel:
log_sigma = np.asarray(log_sigma, dtype=float)
z_centers = np.asarray(z_centers, dtype=float)
if n_layers < 2:
n_layers = 2
edges = np.linspace(
float(z_centers[0]), float(z_centers[-1]), n_layers + 1
)
resistivities = []
thicknesses = []
for layer in range(n_layers):
lo = edges[layer]
hi = edges[layer + 1]
mask = (z_centers >= lo) & (
z_centers <= hi if layer == n_layers - 1 else z_centers < hi
)
if not np.any(mask):
idx = int(np.argmin(np.abs(z_centers - 0.5 * (lo + hi))))
mask[idx] = True
sigma = np.exp(float(np.nanmean(log_sigma[mask])))
resistivities.append(1.0 / max(sigma, 1e-12))
if layer < n_layers - 1:
thicknesses.append(max(float(hi - lo), 1.0))
return StartingModel(resistivities, thicknesses, name="simpeg_1d")
def _layer_centers(thicknesses: np.ndarray) -> np.ndarray:
tops = np.r_[0.0, np.cumsum(thicknesses)]
last = thicknesses[-1] if thicknesses.size else 1.0
bottoms = np.r_[tops[1:], tops[-1] + last]
return 0.5 * (tops + bottoms)
def _station_data(em_data: EMData, idx: int) -> EMData:
return EMData(
method=em_data.method,
frequencies=em_data.frequencies,
rho_a=None if em_data.rho_a is None else _row(em_data.rho_a, idx),
phase=None if em_data.phase is None else _row(em_data.phase, idx),
errors=None if em_data.errors is None else _row(em_data.errors, idx),
station_names=[_station_names(em_data, em_data.n_stations)[idx]],
station_x=np.array(
[_station_x(em_data, em_data.n_stations)[idx]], dtype=float
),
source=em_data.source,
metadata=em_data.metadata_dict(),
)
def _row(arr: np.ndarray, idx: int) -> np.ndarray:
arr = np.asarray(arr, dtype=float)
if arr.ndim == 1:
return arr.copy()
return arr[idx, :].copy()
def _station_names(em_data: EMData, n_st: int) -> list[str]:
if em_data.station_names:
return list(em_data.station_names)
return [f"S{i:03d}" for i in range(n_st)]
def _station_x(em_data: EMData, n_st: int) -> np.ndarray:
if em_data.station_x is not None:
return np.asarray(em_data.station_x, dtype=float)
return np.arange(n_st, dtype=float)
def _station_y(em_data: EMData, n_st: int) -> np.ndarray:
meta = em_data.metadata_dict()
for key in ("station_y", "y", "northing"):
if key in meta:
arr = np.asarray(meta[key], dtype=float)
if arr.size == n_st:
return arr
return np.zeros(n_st, dtype=float)
def _station_locations(em_data: EMData) -> np.ndarray:
n_st = em_data.n_stations
return np.c_[
_station_x(em_data, n_st),
_station_y(em_data, n_st),
np.zeros(n_st, dtype=float),
]
SimPEGBackend.__doc__ = r"""
Run optional SimPEG natural-source EM inversions.
``SimPEGBackend`` adapts PyCSAMT's inversion configuration objects to SimPEG's
natural-source electromagnetic machinery. It is intended for users who want a
physics-backed inversion engine while keeping the same
:class:`pycsamt.inversion.workflow.InversionWorkflow` and
:class:`pycsamt.inversion.results.InversionResult` API used by the built-in and
external backends.
The backend supports MT, AMT, and CSAMT. One-dimensional runs use
``Simulation1DElectricField``. Two-dimensional runs are stitched
station-by-station 1-D inversions, not full 2-D SimPEG EM inversion. Three-
dimensional runs use ``Simulation3DPrimarySecondary`` with plane-wave natural-
source survey objects when available in the installed SimPEG version.
Parameters
----------
config : pycsamt.inversion.config.InversionConfig
Inversion configuration. Important fields are ``method``, ``dimension``,
``data``, ``starting_model``, ``reference_model``, ``regularization``,
``max_iter``, ``tol``, ``error_floor``, ``phase_error``,
``backend_options``, ``workdir``, and ``metadata``.
Attributes
----------
name : str
Backend registry name, always ``"simpeg"``.
supports : tuple of tuple
Supported ``(method, dimension)`` pairs.
Notes
-----
The inversion model is log conductivity, :math:`m = \log(\sigma)`, on SimPEG
mesh cells. Starting and recovered models exposed through PyCSAMT are
resistivity oriented. For 1-D output, recovered cell conductivities are
compressed back into a layered
:class:`pycsamt.inversion.model.StartingModel`. For 3-D output, the result
stores ``rho_3d`` as linear resistivity.
``backend_options`` can contain:
``mesh`` controls
Options consumed by :func:`pycsamt.inversion.mesh.build_1d_tensor_mesh`
and :func:`pycsamt.inversion.mesh.build_3d_tensor_mesh`, such as core
cell widths, padding, depth extent, and station padding.
``max_iter_ls``
Maximum line-search iterations passed to
``optimization.InexactGaussNewton``.
``beta0``, ``estimate_beta``, ``beta0_ratio``
Initial trade-off and beta-estimation controls.
``target_chifact``
Target misfit directive chi factor.
``cooling_factor`` and ``cooling_rate``
Beta schedule directive controls.
``sigma_primary``
Background conductivity for 3-D primary-secondary simulation.
``reference_model``
Cell-sized reference model. Positive values are treated as resistivity and
converted to log conductivity; otherwise values are assumed already in the
SimPEG model domain.
``alpha_y``
Optional y-direction smoothness weight for 3-D regularization.
Examples
--------
Run a 1-D MT sounding directly through the backend::
>>> from pycsamt.inversion.backends.simpeg import SimPEGBackend
>>> from pycsamt.inversion.config import InversionConfig
>>> cfg = InversionConfig(
... method="mt",
... dimension="1d",
... backend="simpeg",
... data={"freqs": [1.0, 10.0],
... "rho_a": [100.0, 120.0],
... "phase": [45.0, 47.0]},
... max_iter=8,
... )
>>> result = SimPEGBackend(cfg).run() # doctest: +SKIP
Run a stitched 2-D AMT profile through the high-level workflow::
>>> from pycsamt.inversion.workflow import run_inversion
>>> result = run_inversion(
... method="amt",
... dimension="2d",
... backend="simpeg",
... data={"freqs": [10.0, 100.0],
... "rho_a": [[80.0, 100.0], [90.0, 110.0]],
... "phase": [[42.0, 45.0], [43.0, 46.0]],
... "station_x": [0.0, 250.0],
... "station_names": ["A01", "A02"]},
... max_iter=6,
... ) # doctest: +SKIP
>>> result.metadata["profile_mode"] # doctest: +SKIP
'stitched_station_1d'
Configure a small 3-D natural-source run::
>>> from pycsamt.inversion.workflow import run_inversion
>>> result = run_inversion(
... method="mt",
... dimension="3d",
... backend="simpeg",
... data={"freqs": [1.0],
... "rho_a": [[100.0], [120.0]],
... "phase": [[45.0], [47.0]],
... "station_x": [0.0, 500.0],
... "station_names": ["M01", "M02"],
... "metadata": {"station_y": [0.0, 250.0]}},
... backend_options={
... "sigma_primary": 0.01,
... "beta0": 1.0,
... "target_chifact": 1.0,
... },
... max_iter=4,
... ) # doctest: +SKIP
Pass SimPEG directive and optimization controls::
>>> from pycsamt.inversion.workflow import run_inversion
>>> result = run_inversion(
... method="csamt",
... dimension="1d",
... backend="simpeg",
... data={"freqs": [1.0, 10.0], "rho_a": [100.0, 120.0]},
... backend_options={
... "estimate_beta": True,
... "beta0_ratio": 2.0,
... "cooling_factor": 2.0,
... "cooling_rate": 1,
... "max_iter_ls": 10,
... },
... ) # doctest: +SKIP
See Also
--------
pycsamt.inversion.workflow.InversionWorkflow
High-level entry point that instantiates this backend.
pycsamt.inversion.backends.builtin.Builtin1DBackend
Dependency-light fallback backend.
pycsamt.inversion.backends.pygimli.PyGIMLiBackend
Optional pyGIMLi backend for 1-D EM inversions.
pycsamt.inversion.mesh.build_1d_tensor_mesh
Mesh helper used by the 1-D SimPEG path.
pycsamt.inversion.mesh.build_3d_tensor_mesh
Mesh helper used by the 3-D SimPEG path.
pycsamt.inversion.results.InversionResult
Backend-neutral result returned by :meth:`run`.
References
----------
.. [1] Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A. and Oldenburg,
D. W. (2015). SimPEG: An open source framework for simulation and
gradient based parameter estimation in geophysical applications.
*Computers & Geosciences*, 85, 142-154.
.. [2] Heagy, L. J., Cockett, R., Kang, S., Rosenkjaer, G. K. and Oldenburg,
D. W. (2017). A framework for simulation and inversion in
electromagnetics. *Computers & Geosciences*, 107, 1-19.
.. [3] Haber, E. (2015). *Computational Methods in Geophysical
Electromagnetics*. SIAM.
"""