pycsamt.forward.maxwell.contracts#

Solver-neutral contracts for frequency-domain Maxwell simulations.

The canonical model order is (z, x) in 2-D and (z, y, x) in 3-D. Canonical impedance output uses (station, frequency, component). This module contains no discretization, sparse solver, or optional backend import.

Classes

ForwardResult(problem_hash, frequencies_hz, ...)

Store canonical impedance predictions from a Maxwell backend.

MaxwellMesh(x_edges_m, z_edges_m[, ...])

Describe a rectilinear finite-volume or finite-element mesh.

MaxwellProblem(mesh, conductivity_s_m, ...)

Define one isotropic frequency-domain MT boundary-value problem.

ReceiverSet(coordinates_m, names[, ...])

Define named receiver locations in mesh coordinates.

SolverDiagnostics(converged, iterations, ...)

Record convergence information for every frequency and source solve.

class pycsamt.forward.maxwell.contracts.MaxwellMesh(x_edges_m, z_edges_m, y_edges_m=None, crs=None)[source]

Bases: object

Describe a rectilinear finite-volume or finite-element mesh.

Parameters:
  • x_edges_m (array-like) – Strictly increasing cell-edge coordinates in metres. Depth z increases downward.

  • z_edges_m (array-like) – Strictly increasing cell-edge coordinates in metres. Depth z increases downward.

  • y_edges_m (array-like or None, optional) – Second horizontal axis. Omit for a 2-D mesh.

  • crs (str or None, optional) – Coordinate reference system for horizontal coordinates.

Examples

>>> mesh = MaxwellMesh([0, 100, 250], [0, 50, 150])
>>> mesh.shape, mesh.dimension
((2, 2), 2)
>>> mesh.cell_centres_m["x"].tolist()
[50.0, 175.0]
x_edges_m: ndarray
z_edges_m: ndarray
y_edges_m: ndarray | None = None
crs: str | None = None
property dimension: int[source]

Return the spatial dimension.

Returns:

Mesh dimension.

Return type:

{2, 3}

Examples

>>> MaxwellMesh([0, 1, 2], [0, 1, 2]).dimension
2
property shape: tuple[int, ...][source]

Return canonical cell-array shape.

Returns:

(nz, nx) or (nz, ny, nx).

Return type:

tuple of int

Examples

>>> MaxwellMesh([0, 1, 2], [0, 1, 2, 3]).shape
(3, 2)
property cell_widths_m: Mapping[str, ndarray][source]

Return read-only cell widths keyed by axis.

Returns:

Keys are x and z, plus y for 3-D.

Return type:

mapping

Examples

>>> mesh = MaxwellMesh([0, 2, 5], [0, 1, 3])
>>> mesh.cell_widths_m["x"].tolist()
[2.0, 3.0]
property cell_centres_m: Mapping[str, ndarray][source]

Return read-only cell centres keyed by axis.

Returns:

Centre coordinates in metres.

Return type:

mapping

Examples

>>> mesh = MaxwellMesh([0, 2, 4], [0, 10, 20])
>>> mesh.cell_centres_m["z"].tolist()
[5.0, 15.0]
to_dict()[source]

Return a JSON-compatible mesh representation.

Returns:

Versioned mesh state.

Return type:

dict

Examples

>>> MaxwellMesh([0, 1, 2], [0, 1, 2]).to_dict()["schema_version"]
1
classmethod from_dict(data)[source]

Restore a validated mesh from serialized state.

Parameters:

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

Returns:

Restored immutable mesh.

Return type:

MaxwellMesh

Examples

>>> mesh = MaxwellMesh([0, 1, 2], [0, 2, 4])
>>> MaxwellMesh.from_dict(mesh.to_dict()).shape
(2, 2)
class pycsamt.forward.maxwell.contracts.ReceiverSet(coordinates_m, names, orientation_deg=0.0)[source]

Bases: object

Define named receiver locations in mesh coordinates.

Parameters:
  • coordinates_m (array-like, shape (n, dimension)) – x,z locations for 2-D or x,y,z locations for 3-D.

  • names (sequence of str) – Unique receiver or station identifiers.

  • orientation_deg (float, default=0.0) – Clockwise rotation of receiver x/y axes from the model axes.

Examples

>>> receivers = ReceiverSet([[50, 0], [150, 0]], ["S00", "S01"])
>>> receivers.dimension, receivers.count
(2, 2)
coordinates_m: ndarray
names: tuple[str, ...]
orientation_deg: float = 0.0
property count: int[source]

Return the number of receivers.

Returns:

Receiver count.

Return type:

int

Examples

>>> ReceiverSet([[0, 0]], ["S00"]).count
1
property dimension: int[source]

Return the coordinate dimension.

Returns:

Number of coordinate columns.

Return type:

{2, 3}

Examples

>>> ReceiverSet([[0, 0, 0]], ["S00"]).dimension
3
to_dict()[source]

Return JSON-compatible receiver state.

Returns:

Versioned receiver coordinates and names.

Return type:

dict

Examples

>>> ReceiverSet([[0, 0]], ["S00"]).to_dict()["names"]
['S00']
classmethod from_dict(data)[source]

Restore receivers from serialized state.

Parameters:

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

Returns:

Validated receiver collection.

Return type:

ReceiverSet

Examples

>>> state = ReceiverSet([[0, 0]], ["S00"]).to_dict()
>>> ReceiverSet.from_dict(state).names
('S00',)
class pycsamt.forward.maxwell.contracts.MaxwellProblem(mesh, conductivity_s_m, frequencies_hz, receivers, components=('zxy', 'zyx'), active_cells=None, time_dependence='exp(+iwt)', magnetic_permeability_h_m=1.2566370614359173e-06, metadata=<factory>)[source]

Bases: object

Define one isotropic frequency-domain MT boundary-value problem.

Parameters:
  • mesh (MaxwellMesh) – Rectilinear simulation mesh.

  • conductivity_s_m (array-like) – Positive isotropic conductivity in S/m, shaped like mesh.

  • frequencies_hz (array-like, shape (n_frequency,)) – Positive, unique frequencies. Input order is retained.

  • receivers (ReceiverSet) – Observation locations with the same dimension as mesh.

  • components (sequence of {"zxx", "zxy", "zyx", "zyy"}) – Requested impedance components in explicit output order.

  • active_cells (array-like of bool or None, optional) – Cells participating in the physical model. Air cells can remain active with a small conductivity or be marked inactive for capable backends.

  • time_dependence ({"exp(+iwt)", "exp(-iwt)"}, default="exp(+iwt)") – Complex phasor convention.

  • magnetic_permeability_h_m (float, default=1.25663706212e-6) – Uniform scalar permeability in H/m.

  • metadata (mapping, optional) – Finite JSON-compatible provenance; never interpreted by a backend.

Examples

>>> mesh = MaxwellMesh([0, 100, 200], [0, 50, 100])
>>> receivers = ReceiverSet([[50, 0]], ["S00"])
>>> problem = MaxwellProblem(
...     mesh, np.full(mesh.shape, 0.01), [10, 1], receivers
... )
>>> problem.problem_hash == problem.problem_hash
True
mesh: MaxwellMesh
conductivity_s_m: ndarray
frequencies_hz: ndarray
receivers: ReceiverSet
components: tuple[str, ...] = ('zxy', 'zyx')
active_cells: ndarray | None = None
time_dependence: str = 'exp(+iwt)'
magnetic_permeability_h_m: float = 1.2566370614359173e-06
metadata: Mapping[str, Any]
property problem_hash: str[source]

Return a deterministic SHA-256 digest of all physical inputs.

Returns:

Digest suitable for cache keys.

Return type:

str

Examples

>>> mesh = MaxwellMesh([0, 1, 2], [0, 1, 2])
>>> p = MaxwellProblem(
...     mesh, np.ones(mesh.shape), [1], ReceiverSet([[0, 0]], ["S"])
... )
>>> len(p.problem_hash)
64
provenance()[source]

Return JSON-compatible problem provenance excluding large arrays.

Returns:

Mesh, receiver, convention, components, and metadata state.

Return type:

dict

Examples

>>> mesh = MaxwellMesh([0, 1, 2], [0, 1, 2])
>>> p = MaxwellProblem(
...     mesh, np.ones(mesh.shape), [1], ReceiverSet([[0, 0]], ["S"])
... )
>>> p.provenance()["components"]
['zxy', 'zyx']
to_npz(path)[source]

Write a pickle-free problem archive.

Parameters:

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

Returns:

Destination path.

Return type:

pathlib.Path

Examples

>>> from tempfile import TemporaryDirectory
>>> p = MaxwellProblem(
...     MaxwellMesh([0, 1, 2], [0, 1, 2]),
...     np.ones((2, 2)),
...     [1],
...     ReceiverSet([[0, 0]], ["S"]),
... )
>>> with TemporaryDirectory() as d:
...     restored = MaxwellProblem.from_npz(p.to_npz(Path(d) / "p.npz"))
>>> restored.problem_hash == p.problem_hash
True
classmethod from_npz(path)[source]

Restore and validate a problem archive.

Parameters:

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

Returns:

Restored problem.

Return type:

MaxwellProblem

Examples

>>> from tempfile import TemporaryDirectory
>>> p = MaxwellProblem(
...     MaxwellMesh([0, 1, 2], [0, 1, 2]),
...     np.ones((2, 2)),
...     [1],
...     ReceiverSet([[0, 0]], ["S"]),
... )
>>> with TemporaryDirectory() as d:
...     q = MaxwellProblem.from_npz(p.to_npz(Path(d) / "p.npz"))
>>> np.array_equal(q.conductivity_s_m, p.conductivity_s_m)
True
class pycsamt.forward.maxwell.contracts.SolverDiagnostics(converged, iterations, relative_residual, runtime_s, messages=())[source]

Bases: object

Record convergence information for every frequency and source solve.

Parameters:
  • converged (array-like of bool, shape (n_frequency, n_source)) – Whether each linear solve met its tolerance.

  • iterations (array-like of int, same shape) – Iteration count; zero is valid for direct solvers.

  • relative_residual (array-like of float, same shape) – Final non-negative relative residual.

  • runtime_s (float) – Total non-negative solver runtime in seconds.

  • messages (sequence of str, optional) – Backend messages for failed or noteworthy solves.

Examples

>>> d = SolverDiagnostics(
...     [[True], [False]], [[4], [20]], [[1e-8], [1e-2]], 0.5
... )
>>> d.success, d.maximum_relative_residual
(False, 0.01)
converged: ndarray
iterations: ndarray
relative_residual: ndarray
runtime_s: float
messages: tuple[str, ...] = ()
property success: bool[source]

Return whether every solve converged.

Returns:

True only when all convergence flags are true.

Return type:

bool

Examples

>>> SolverDiagnostics([[True]], [[1]], [[1e-9]], 0).success
True
property maximum_relative_residual: float[source]

Return the largest reported relative residual.

Returns:

Worst solve residual.

Return type:

float

Examples

>>> SolverDiagnostics(
...     [[True]], [[1]], [[1e-7]], 0
... ).maximum_relative_residual
1e-07
to_dict()[source]

Return JSON-compatible convergence diagnostics.

Returns:

Versioned diagnostic state.

Return type:

dict

Examples

>>> SolverDiagnostics([[True]], [[2]], [[1e-8]], 0.1).to_dict()[
...     "runtime_s"
... ]
0.1
classmethod from_dict(data)[source]

Restore validated convergence diagnostics.

Parameters:

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

Returns:

Restored diagnostic record.

Return type:

SolverDiagnostics

Examples

>>> state = SolverDiagnostics([[True]], [[2]], [[1e-8]], 0.1).to_dict()
>>> SolverDiagnostics.from_dict(state).success
True
class pycsamt.forward.maxwell.contracts.ForwardResult(problem_hash, frequencies_hz, receiver_names, components, impedance_v_a, valid, backend_name, backend_version, diagnostics, metadata=<factory>)[source]

Bases: object

Store canonical impedance predictions from a Maxwell backend.

Parameters:
  • problem_hash (str) – Hash of the exact MaxwellProblem solved.

  • frequencies_hz (array-like) – Frequency vector in problem order.

  • receiver_names (sequence of str) – Explicit station and tensor-component axes.

  • components (sequence of str) – Explicit station and tensor-component axes.

  • impedance_v_a (complex array, shape (station, frequency, component)) – Predicted SI impedance.

  • valid (bool array or None, optional) – Validity mask with the same shape. Defaults to finite predictions.

  • backend_name (str) – Solver identity required for reproducibility.

  • backend_version (str) – Solver identity required for reproducibility.

  • diagnostics (SolverDiagnostics) – Per-solve convergence record.

  • metadata (mapping, optional) – Additional finite JSON-compatible backend provenance.

Examples

>>> d = SolverDiagnostics([[True]], [[3]], [[1e-9]], 0.01)
>>> r = ForwardResult(
...     "a" * 64, [1], ["S"], ["zxy"], [[[1 + 2j]]], None, "demo", "1", d
... )
>>> r.shape, r.success
((1, 1, 1), True)
problem_hash: str
frequencies_hz: ndarray
receiver_names: tuple[str, ...]
components: tuple[str, ...]
impedance_v_a: ndarray
valid: ndarray | None
backend_name: str
backend_version: str
diagnostics: SolverDiagnostics
metadata: Mapping[str, Any]
property shape: tuple[int, int, int][source]

Return canonical impedance shape.

Returns:

(station, frequency, component).

Return type:

tuple of int

Examples

>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> ForwardResult(
...     "0" * 64, [1], ["S"], ["zxy"], [[[1j]]], None, "b", "1", d
... ).shape
(1, 1, 1)
property success: bool[source]

Return whether all solves converged and predictions are valid.

Returns:

Combined numerical and observation validity status.

Return type:

bool

Examples

>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> ForwardResult(
...     "0" * 64, [1], ["S"], ["zxy"], [[[1j]]], None, "b", "1", d
... ).success
True
validate_against(problem)[source]

Raise if this result does not exactly match a problem contract.

Parameters:

problem (MaxwellProblem) – Expected input problem.

Raises:

ValueError – If hash or any output axis differs.

Return type:

None

Examples

>>> mesh = MaxwellMesh([0, 1, 2], [0, 1, 2])
>>> p = MaxwellProblem(
...     mesh,
...     np.ones((2, 2)),
...     [1],
...     ReceiverSet([[0, 0]], ["S"]),
...     ("zxy",),
... )
>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> ForwardResult(
...     p.problem_hash,
...     [1],
...     ["S"],
...     ["zxy"],
...     [[[1j]]],
...     None,
...     "b",
...     "1",
...     d,
... ).validate_against(p)
provenance()[source]

Return JSON-compatible solver and output-axis provenance.

Returns:

Problem identity, axes, backend, diagnostics, and metadata.

Return type:

dict

Examples

>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> r = ForwardResult(
...     "0" * 64, [1], ["S"], ["zxy"], [[[1j]]], None, "b", "1", d
... )
>>> r.provenance()["backend_name"]
'b'
to_npz(path)[source]

Write a versioned, pickle-free result archive.

Parameters:

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

Returns:

Destination path.

Return type:

pathlib.Path

Examples

>>> from tempfile import TemporaryDirectory
>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> r = ForwardResult(
...     "0" * 64, [1], ["S"], ["zxy"], [[[1j]]], None, "b", "1", d
... )
>>> with TemporaryDirectory() as directory:
...     restored = ForwardResult.from_npz(
...         r.to_npz(Path(directory) / "r.npz")
...     )
>>> restored.backend_name, restored.shape
('b', (1, 1, 1))
classmethod from_npz(path)[source]

Restore and validate a result archive without pickle.

Parameters:

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

Returns:

Restored canonical result.

Return type:

ForwardResult

Examples

>>> from tempfile import TemporaryDirectory
>>> d = SolverDiagnostics([[True]], [[0]], [[0]], 0)
>>> r = ForwardResult(
...     "0" * 64, [1], ["S"], ["zxy"], [[[1j]]], None, "b", "1", d
... )
>>> with TemporaryDirectory() as directory:
...     restored = ForwardResult.from_npz(
...         r.to_npz(Path(directory) / "r.npz")
...     )
>>> np.array_equal(restored.impedance_v_a, r.impedance_v_a)
True