pycsamt.core#

Core configuration, base objects, registries, adapters, and mixins shared across pyCSAMT v2.

Core survey objects, configuration, registries, and transfer-function data.

class pycsamt.core.CoreObject#

Bases: PyCSAMTObject

Minimal base class for PyCSAMT objects and mixins.

The goal is to provide stable, readable __repr__ and __str__ implementations, plus a couple of helpers for light introspection and serialization that do not pull heavy dependencies.

Notes

  • __repr__ shows a compact field summary. Large arrays or containers are summarized safely (shape/len only).

  • Subclasses may override _repr_fields() to control which attributes appear in the summary.

  • as_dict() is shallow by default and avoids importing external libs. It aims to be robust, not exhaustive.

Examples

>>> class P(CoreObject):
...     def __init__(self):
...         self.name = "S001"
...         self.data = [1, 2, 3]
...
>>> P()
P(name='S001', data=[len=3])

See also

_repr_fields

Hook to pick fields for __repr__.

as_dict

Lightweight serialization helper.

summary

Short single-line description.

summary(*, max_fields=6)#

Return a short one-line description.

Parameters:

max_fields (int, optional) – Maximum number of fields to include.

Returns:

A compact summary string.

Return type:

str

as_dict(*, public_only=True, max_depth=1)#

Serialize to a lightweight dict safely.

Parameters:
  • public_only (bool, optional) – If True, include only public attributes (no leading underscore). Dataclasses always use their declared fields.

  • max_depth (int, optional) – Maximum recursion depth for nested mappings and sequences. Use small values to avoid deep walks.

Returns:

A JSON-friendly mapping (best-effort).

Return type:

dict

class pycsamt.core.MTBase#

Bases: CoreObject

Common electromagnetic and MT utilities.

This mixin-like base provides numerically safe, vectorized helpers built on NumPy. All methods accept array-like inputs and return NumPy arrays with broadcast semantics.

Notes

Shapes are preserved and operations are element-wise unless stated otherwise. For impedance tensors shaped (..., 2, 2), “determinant” operations collapse the last two axes.

Unit conventions. Two equivalent formulas are common for apparent resistivity from impedance magnitude |Z|:

  • SI (E in V/m, H in A/m): ρₐ = |Z|² / (μ₀·ω) = |Z|² · RHO_FACTOR / f, with ω = f and RHO_FACTOR = 1/(μ₀·2π).

  • Field units (E in mV/km, B in nT): ρₐ (0.2 / f) · |E/H|² — a legacy Zonge-style form. This is reflected by ZONGE_RHO_FACTOR, i.e. 0.2.

Be consistent. Converting mV/km → V/m and nT → T, and B → H via H = B/μ₀, recovers the SI formulation.

Constants#

MU0float

Magnetic permeability of free space (H/m). × 10⁻⁷.

EPS0float

Electric permittivity of free space (F/m). 8.854187817 × 10⁻¹².

Cfloat

Speed of light in vacuum (m/s). From CODATA.

C0float

1/√(μ₀·ε₀) (m/s). Equal to C by definition.

ETA0float

Wave impedance of free space (Ω). √(μ₀/ε₀).

TWO_PIfloat

. Handy for ω = f conversions.

DEG2RADfloat

Degrees to radians. π/180.

RAD2DEGfloat

Radians to degrees. 180/π.

RHO_FACTORfloat

1/(μ₀·2π). For ρₐ = |Z|² · RHO_FACTOR / f (SI).

ZONGE_RHO_FACTORfloat

0.2. Legacy factor in ρₐ (0.2/f)·|E/H|² when using E in mV/km and B/H in nT without explicit SI conversion in the formula.

B_TO_Hfloat

Conversion factor from magnetic flux density to field intensity (A/m per Tesla). 1/μ₀.

# Unit scales (field practice) MICROVOLTS_TO_VOLTS : float

1e-6.

PICOTESLA_TO_TESLAfloat

1e-12.

NANOTESLA_TO_TESLAfloat

1e-9.

MV_PER_KM_TO_V_PER_Mfloat

1e-6. Since 1 mV/km = 1e-3 V / 1e3 m.

METERS_TO_KILOMETERSfloat

1e-3.

PERCENT_FACTORfloat

100.0. For percent amplitudes or coherence.

Z_UNIT_MVK_NT_TO_SIfloat

(1e-6) / (1e-9) = 1e3. Converts Z expressed as (mV/km)/nT into SI-consistent (V/m)/T.

See also

rho_phase_from_z

Apparent resistivity and phase from Z.

z_from_rho_phase

Build Z magnitude/phase from ρ, φ.

determinant_z

Rotation-invariant impedance.

rho_phase_from_det

Invariants from determinant impedance.

rotate_impedance

Rotate 2×2 impedance tensors.

skin_depth

Diffusive skin depth.

tipper_amp_phase

Amplitude and phase of tipper vector.

Examples

Compute SI apparent resistivity from |Z| and frequency:

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Zmag = np.asarray([5.0, 10.0])        # |Z| in ohms
>>> f = np.asarray([1.0, 0.1])            # Hz
>>> rho_a = (Zmag**2) * MTBase.RHO_FACTOR / f
>>> rho_a.shape
(2,)

Convert field units to SI and compare formulas:

>>> E_mVkm = 100.0      # mV/km
>>> B_nT = 50.0         # nT (≈ μ0·H if not converted)
>>> Z_field = (E_mVkm * MTBase.MV_PER_KM_TO_V_PER_M) / \
...           (B_nT * MTBase.NANOTESLA_TO_TESLA)
>>> Z_si_like = Z_field  # (V/m)/T
>>> f = 1.0  # Hz
>>> rho_si = (Z_si_like**2) * MTBase.RHO_FACTOR / f
>>> rho_zonge = (MTBase.ZONGE_RHO_FACTOR / f) * (E_mVkm / B_nT)**2
>>> rho_si > 0 and rho_zonge > 0
True

Rotate an impedance tensor by a site azimuth:

>>> Z = np.zeros((3, 2, 2), dtype=complex)  # demo only
>>> phi = 30.0 * MTBase.DEG2RAD
>>> # rotate_impedance would apply R(φ) Z Rᵀ(φ)
>>> # Z_rot = MTBase.rotate_impedance(Z, phi)  # hypothetical

References

MU0: float = 1.2566370614359173e-06#
EPS0: float = 8.854187817e-12#
C: float = 299792458.0#
C0: float = np.float64(299792458.0105029)#
ETA0: float = np.float64(376.7303134749689)#
TWO_PI: float = 6.283185307179586#
DEG2RAD: float = 0.017453292519943295#
RAD2DEG: float = 57.29577951308232#
RHO_FACTOR: float = 126651.47955292223#
ZONGE_RHO_FACTOR: float = 0.2#
MICROVOLTS_TO_VOLTS: float = 1e-06#
MILLIVOLTS_TO_VOLTS: float = 0.001#
MV_PER_KM_TO_V_PER_M: float = 1e-06#
PICOTESLA_TO_TESLA: float = 1e-12#
NANOTESLA_TO_TESLA: float = 1e-09#
MICROTESLA_TO_TESLA: float = 1e-06#
H_TO_B: float = 1.2566370614359173e-06#
B_TO_H: float = 795774.7154594767#
METERS_TO_KILOMETERS: float = 0.001#
KILOMETERS_TO_METERS: float = 1000.0#
PERCENT_FACTOR: float = 100.0#
Z_UNIT_MVK_NT_TO_SI: float = 1000.0#
static omega(f)#

Angular frequency ω = f.

Parameters:

f (object)

Return type:

ndarray

rho_phase_from_z(z, f, *, phase_unit='deg')#

Apparent resistivity and phase from impedance.

Parameters:
  • z (array_like) – Impedance, scalar or tensor (..., 2, 2).

  • f (array_like) – Frequency (Hz), broadcastable with z.

  • phase_unit ({'deg', 'rad', 'mrad'}, optional) – Output unit for phase. Default is degrees.

Returns:

  • rho (ndarray) – Apparent resistivity (Ω·m), element-wise.

  • phi (ndarray) – Phase in requested unit, element-wise.

Return type:

tuple[ndarray, ndarray]

Notes

Uses ρa = |Z|^2 / (μ0 ω) per component. For tensor invariants see rho_phase_from_det().

z_from_rho_phase(rho, phi, f, *, phase_unit='deg')#

Build impedance magnitude/phase from ρa and φ.

Parameters:
  • rho (array_like) – Apparent resistivity (Ω·m).

  • phi (array_like) – Phase in phase_unit.

  • f (array_like) – Frequency (Hz).

  • phase_unit ({'deg', 'rad', 'mrad'}, optional) – Unit of phi. Default is degrees.

Returns:

Z – Complex impedance with |Z| = sqrt(μ0 ω ρa) and angle = φ. Shape follows broadcasting rules.

Return type:

ndarray

static determinant_z(z)#

Determinant impedance Z_det = sqrt(-Z_xy Z_yx).

Parameters:

z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

Returns:

Z_det – Rotation-invariant complex impedance.

Return type:

ndarray, shape (…)

Notes

The minus sign assumes the common MT sign convention.

rho_phase_from_det(z, f, *, phase_unit='deg')#

Invariant apparent resistivity and phase from Z.

Parameters:
  • z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

  • f (array_like) – Frequency (Hz).

  • phase_unit ({'deg', 'rad', 'mrad'}, optional) – Output unit for phase.

Returns:

  • rho_det (ndarray) – Determinant apparent resistivity (Ω·m).

  • phi_det (ndarray) – Determinant phase in requested unit.

Return type:

tuple[ndarray, ndarray]

static rotate_impedance(z, theta_deg)#

Rotate 2×2 impedance tensor(s) by theta_deg.

Parameters:
  • z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

  • theta_deg (float) – Rotation angle in degrees. Positive rotates the coordinate frame clockwise (convention).

Returns:

Z_rot – Rotated impedance tensors.

Return type:

ndarray, shape (…, 2, 2)

Notes

Uses Z' = R Z Rᵀ with

R = [[cos θ,  sin θ], [-sin θ, cos θ]].

skin_depth(f, rho)#

Skin depth δ = sqrt(2ρ / (μ0 ω)) in meters.

Parameters:
  • f (array_like) – Frequency (Hz).

  • rho (array_like) – Resistivity (Ω·m).

Returns:

δ – Skin depth in meters.

Return type:

ndarray

Notes

For quick estimates, δ 503 √(ρ/f) (ρ in Ω·m, f in Hz).

static freq_to_period(f)#

Convert frequency (Hz) to period (s), safe at zero.

Parameters:

f (object)

Return type:

ndarray

static period_to_freq(t)#

Convert period (s) to frequency (Hz), safe at inf.

Parameters:

t (object)

Return type:

ndarray

static tipper_amp_phase(t, *, phase_unit='deg')#

Amplitude and phase of tipper vectors.

Parameters:
  • t (array_like, shape (..., 2) or (..., 1, 2)) – Tipper components (Tx, Ty).

  • phase_unit ({'deg', 'rad', 'mrad'}, optional) – Output unit for phase. Default is degrees.

Returns:

  • amp (ndarray) – Vector magnitude sqrt(Tx² + Ty²).

  • phi (ndarray) – Phase of the complex vector Tx + i Ty.

Return type:

tuple[ndarray, ndarray]

Notes

If shape is (..., 1, 2) it is squeezed to (..., 2) prior to computation.

phase_tensor(z)#

Phase tensor Φ = X⁺ Y from impedance Z = X + iY.

This computes a real 2×2 phase tensor using a robust pseudoinverse X⁺ of the real part of Z. Shapes are broadcast over the leading axes.

Parameters:

z (array_like, shape (..., 2, 2)) – Impedance tensor(s) Z = X + iY.

Returns:

Phi – Real phase tensor.

Return type:

ndarray, shape (…, 2, 2)

Notes

If X is ill-conditioned, the pseudoinverse stabilizes the solution. Φ is invariant to site gain and captures the 2-D/3-D character of the response.

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Z = np.array([[[0+1j, 2+3j],[4+5j, 6+7j]]], dtype=complex)
>>> Phi = MTBase().phase_tensor(Z)
>>> Phi.shape
(1, 2, 2)
phase_tensor_params(z, *, angle_unit='deg')#

Principal phase angles, azimuth, skew, and ellipticity.

Derives quick-look parameters from the phase tensor Φ: principal phase angles (φ_max, φ_min), phase tensor azimuth α, skew angle β, and ellipticity.

Parameters:
  • z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

  • angle_unit ({'deg', 'rad', 'mrad'}, optional) – Output unit for angular quantities. Default is degrees.

Returns:

  • phi_max (ndarray, shape (…)) – Larger principal phase angle derived from Φ.

  • phi_min (ndarray, shape (…)) – Smaller principal phase angle derived from Φ.

  • alpha (ndarray, shape (…)) – Phase tensor azimuth.

  • beta (ndarray, shape (…)) – Phase tensor skew angle.

  • ellipt (ndarray, shape (…)) – Ellipticity proxy (λ_max - λ_min)/(λ_max + λ_min), where λ are the eigenvalues used internally.

Return type:

tuple[ndarray, ndarray, ndarray, ndarray, ndarray]

Notes

This implementation obtains parameters from an eigen decomposition of Φ and simple angle formulas (Caldwell- style azimuth/skew heuristics). It is suitable for quality control and strike reconnaissance.

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Z = np.zeros((5, 2, 2), complex)
>>> z = MTBase()
>>> pmax, pmin, az, skew, eps = z.phase_tensor_params(Z)
>>> pmax.shape == pmin.shape == az.shape == skew.shape == eps.shape
True
phase_tensor_azimuth(z, *, unit='deg')#

Azimuth of the phase tensor principal axis.

Parameters:
  • z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

  • unit ({'deg', 'rad', 'mrad'}, optional) – Output unit. Default is degrees.

Returns:

alpha – Phase tensor azimuth.

Return type:

ndarray, shape (…)

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Z = np.zeros((3, 2, 2), complex)
>>> MTBase().phase_tensor_azimuth(Z).shape
(3,)
static tipper_rotate(t, theta_deg)#

Rotate tipper vectors by theta_deg in the horizontal plane.

Parameters:
  • t (array_like, shape (..., 2) or (..., 1, 2)) – Tipper components (T_x, T_y). A trailing (1, 2) is squeezed.

  • theta_deg (float) – Rotation angle in degrees. Positive angles rotate the coordinates clockwise (same convention as rotate_impedance()).

Returns:

t_rot – Rotated tipper vectors.

Return type:

ndarray, shape (…, 2)

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> T = np.array([[0.2+0.1j, -0.1+0.3j]])
>>> MTBase.tipper_rotate(T, 30.0).shape
(1, 2)
static induction_arrows(t, *, convention='wiese', use_imag=False)#

Compute 2-D induction arrows from tipper components.

Parameters:
  • t (array_like, shape (..., 2) or (..., 1, 2)) – Tipper components (T_x, T_y).

  • convention ({'wiese', 'parkinson'}, optional) – Arrow convention. 'wiese' uses a 90° rotation from the real (or imaginary) tipper; 'parkinson' uses the components directly. Default 'wiese'.

  • use_imag (bool, optional) – If True, use imaginary parts; else use real parts. Default False.

Returns:

  • ax (ndarray, shape (…)) – X-component of the arrow.

  • ay (ndarray, shape (…)) – Y-component of the arrow.

Return type:

tuple[ndarray, ndarray]

Notes

Wiese arrows often highlight current channeling (2-D/3-D). Use imaginary arrows (use_imag=True) for deeper structure emphasis at higher periods.

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> T = np.array([[0.2+0.1j, -0.1+0.3j]])
>>> ax, ay = MTBase.induction_arrows(T, convention='wiese')
>>> ax.shape == ay.shape
True

See also

tipper_rotate

swift_skew(z, *, unit='deg')#

Swift skew parameter from the impedance tensor.

Computes the complex Swift skew ratio

s = (Z_xx + Z_yy) / (Z_xy - Z_yx),

returning its magnitude and angle.

Parameters:
  • z (array_like, shape (..., 2, 2)) – Impedance tensor(s).

  • unit ({'deg', 'rad', 'mrad'}, optional) – Unit for the returned angle. Default is degrees.

Returns:

  • amp (ndarray, shape (…)) – Magnitude |s| (dimensionless).

  • ang (ndarray, shape (…)) – Argument of s in the requested unit.

Return type:

tuple[ndarray, ndarray]

Notes

Low |s| values are consistent with 1-D/2-D structure; an elevated skew suggests 3-D effects or distortion.

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Z = np.ones((4, 2, 2), dtype=complex)
>>> amp, ang = MTBase().swift_skew(Z)
>>> amp.shape == ang.shape
True
apparent_conductivity_from_z(z, f)#

Apparent conductivity σₐ = μ₀ ω / |Z|² (S/m).

Parameters:
  • z (array_like) – Impedance (per component or invariant).

  • f (array_like) – Frequency (Hz), broadcastable to z.

Returns:

sigma_a – Apparent conductivity (S/m).

Return type:

ndarray

Notes

This is the reciprocal of the common SI resistivity formula ρₐ = |Z|² / (μ₀ ω).

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> Z = np.array([5.0, 10.0])       # Ω
>>> f = np.array([1.0, 0.1])        # Hz
>>> MTBase().apparent_conductivity_from_z(Z, f).shape
(2,)
halfspace_impedance(f, rho)#

Plane-wave impedance over a uniform half-space.

For a homogeneous half-space of resistivity ρ, the surface impedance is

Z = (1 + i) / √2 · √(μ₀ ω ρ),

with a constant 45° phase and amplitude √(ω ρ).

Parameters:
  • f (array_like) – Frequency (Hz).

  • rho (array_like) – Resistivity (Ω·m).

Returns:

Z – Complex half-space impedance.

Return type:

ndarray

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> f = np.logspace(-3, 3, 5)
>>> Z = MTBase().halfspace_impedance(f, 100.0)
>>> Z.shape
(5,)
z_mvk_nt_to_ohms(z)#

Convert Z from (mV/km)/nT to ohms (Ω).

This helper assumes the denominator is magnetic flux density B in nT (not H). It converts to SI (V/m)/T and then multiplies by μ₀ to obtain V/A = Ω.

Parameters:

z (array_like) – Impedance in mixed field units (mV/km)/nT.

Returns:

Z_ohms – Impedance in ohms (Ω).

Return type:

ndarray

Notes

If your denominator is already the magnetic field intensity H in A/m, you should not multiply by μ₀. Use careful unit bookkeeping in mixed-unit workflows.

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> z_field = np.array([50.0])  # (mV/km)/nT
>>> Z_ohm = MTBase().z_mvk_nt_to_ohms(z_field)
>>> Z_ohm.shape
(1,)
static rotate_fields(e, h, theta_deg)#

Rotate horizontal electric and magnetic field vectors.

Parameters:
  • e (array_like, shape (..., 2)) – Horizontal electric field components (E_x, E_y).

  • h (array_like, shape (..., 2)) – Horizontal magnetic field components (H_x, H_y).

  • theta_deg (float) – Rotation angle in degrees. Positive rotates the coordinate frame clockwise.

Returns:

  • e_rot (ndarray, shape (…, 2)) – Rotated electric field components.

  • h_rot (ndarray, shape (…, 2)) – Rotated magnetic field components.

Return type:

tuple[ndarray, ndarray]

Examples

>>> from pycsamt.core.base import MTBase
>>> import numpy as np
>>> e = np.array([[1.0, 0.0]])
>>> h = np.array([[0.0, 1.0]])
>>> e_r, h_r = MTBase.rotate_fields(e, h, 45.0)
>>> e_r.shape == h_r.shape
True
class pycsamt.core.TFBundle(freq=None, z=None, z_err=None, tipper=None, tipper_err=None, rho=None, phase=None, station=None, station_id=None, lat=None, lon=None, elev=None, azimuth=None, attrs=<factory>)#

Bases: CoreObject

Lightweight, neutral payload for transfer functions.

The bundle holds frequency-indexed arrays and optional site metadata. It is intentionally permissive about array types (e.g., lists or NumPy arrays) so that backends can populate and consume it without hard dependencies.

Parameters:
  • freq (array_like or None) – Frequency vector, length n.

  • z (array_like or None) – Impedance tensor, shape (n, 2, 2) (complex).

  • z_err (array_like or None) – Errors for z, shape (n, 2, 2) (float).

  • tipper (array_like or None) – Magnetic tipper. Common shapes are (n, 2) or (n, 1, 2); callers may normalize as needed.

  • tipper_err (array_like or None) – Errors for the tipper, shape like tipper.

  • rho (array_like or None) – Apparent resistivity, length n (float).

  • phase (array_like or None) – Phase, length n. Units are backend-specific; the Zonge path often stores milliradians.

  • station (str or None) – Preferred station/site name.

  • station_id (str, int or None) – Identifier used to synthesize a name when missing.

  • lat (float or None) – Optional geographic location and elevation.

  • lon (float or None) – Optional geographic location and elevation.

  • elev (float or None) – Optional geographic location and elevation.

  • azimuth (float or None) – Sensor azimuth or site orientation (degrees).

  • attrs (dict) – Free-form attributes that accompany the data.

Notes

Only one of z or (rho, phase) is required for most workflows. Transformers may fill the missing part using package settings (see pycsamt.core.config).

Examples

>>> from pycsamt.core.base import TFBundle
>>> TFBundle(freq=[1.0], z=[[ [0+1j, 0], [0, 0+1j] ]],
...          station=\"S001\")
TFBundle(...)
freq: Any | None = None#
z: Any | None = None#
z_err: Any | None = None#
tipper: Any | None = None#
tipper_err: Any | None = None#
rho: Any | None = None#
phase: Any | None = None#
station: str | None = None#
station_id: str | int | None = None#
lat: float | None = None#
lon: float | None = None#
elev: float | None = None#
azimuth: float | None = None#
attrs: dict[str, Any]#
is_empty()#

Return True if there is no usable TF content.

A bundle is considered empty when neither z nor the pair (rho, phase) is present.

Returns:

True if empty, False otherwise.

Return type:

bool

Examples

>>> TFBundle().is_empty()
True
>>> TFBundle(rho=[100], phase=[45]).is_empty()
False
pycsamt.core.to_edi(source, *, key=None, **kwargs)#

Dispatch source to a registered adapter and return EDI.

This function consults the adapter registry managed by pycsamt.core.config. It attempts to infer the adapter key when not provided and calls the associated factory.

Parameters:
  • source (Any) – The object to convert (AVG, Jones, etc.).

  • key (str, optional) – Adapter key (e.g., 'avg', 'j', 'edi'). If omitted, pick_adapter_key() is used.

  • **kwargs (Any) – Extra keyword arguments forwarded to the adapter.

Returns:

An EDI object or an EDI collection, depending on the adapter.

Return type:

Any

Raises:

RuntimeError – If the key cannot be inferred or no adapter is registered for it.

See also

pycsamt.core.config.register_adapter

Register new adapters at runtime.

pick_adapter_key

Lightweight key inference from object metadata.

Examples

Register a trivial adapter and convert an object:

>>> from pycsamt.core.config import register_adapter
>>> class Dummy: pass
>>> Dummy.__module__ = 'pycsamt.zonge.avg'
>>> register_adapter('avg', lambda src, **k: {'edi': True})
>>> to_edi(Dummy())
{'edi': True}
exception pycsamt.core.RegistryError#

Bases: RuntimeError

Error raised for registry and manifest operations.

Use this for signaling inconsistencies, corruption, or unsupported formats detected while loading or saving manifests or while mutating registry content.

class pycsamt.core.Record(rid, kind, path=None, fmt=None, dataid=None, station_id=None, tags=<factory>, meta=<factory>, checksum=None, created=<factory>, updated=<factory>)#

Bases: CoreObject

A single registry item with identifiers and metadata.

Parameters:
  • rid (str) – Unique record id. Often a value from _uuid().

  • kind (str) – Logical category. See guess_kind() for common labels (e.g. "edi", "avg").

  • path (str or None, optional) – Path (relative to Manifest.root or absolute) to the primary file associated with this record.

  • fmt (str or None, optional) – Short format tag (e.g. "edi" or "json"). Free form and optional.

  • dataid (str or None, optional) – External data id or survey id if applicable.

  • station_id (str or None, optional) – Station or site identifier for field data.

  • tags (list of str, optional) – Free-form labels for grouping or filtering.

  • meta (dict, optional) – Arbitrary JSON-serializable metadata.

  • checksum (str or None, optional) – SHA-256 hex digest of the file at path. Use _sha256() to compute it. Optional.

  • created (float, optional) – Creation timestamp in seconds since epoch. Defaults to _now().

  • updated (float, optional) – Last update timestamp in seconds since epoch. Defaults to _now().

Variables:

Notes

The class is a dataclasses dataclass and is designed to round-trip with to_dict() and from_dict().

Examples

Create a record with a checksum:

from pathlib import Path
p = Path("data/site001.edi")
r = Record(rid=_uuid(), kind="edi", path=str(p),
           checksum=_sha256(p))

See also

Manifest, guess_kind

rid: str#
kind: str#
path: str | None = None#
fmt: str | None = None#
dataid: str | None = None#
station_id: str | None = None#
tags: list[str]#
meta: dict[str, Any]#
checksum: str | None = None#
created: float#
updated: float#
touch()#

Bump updated to the current time.

Notes

Uses _now(). Does not mutate any other fields.

Return type:

None

to_dict()#

Return a JSON-ready mapping for this record.

Returns:

A plain dict produced via dataclasses.asdict().

Return type:

dict

classmethod from_dict(d)#

Create a Record from a mapping.

Parameters:

d (dict) – Mapping with the same keys as to_dict().

Returns:

New instance populated from the mapping.

Return type:

Record

class pycsamt.core.Manifest(root, version=1, records=<factory>)#

Bases: CoreObject

Container mapping record ids to Record objects.

Parameters:
  • root (str) – Base directory for paths stored in records. Callers may join this with Record.path.

  • version (int, optional) – Schema or persistence version. Defaults to 1.

  • records (dict[str, Record], optional) – Mapping of record id to Record. Defaults to an empty mapping.

Variables:

Notes

The manifest is serializable through to_dict() and from_dict(). Storage format (JSON, TOML, etc.) is left to ManifestStore implementations.

Examples

Build a manifest programmatically:

man = Manifest(root="data")
rec = Record(rid=_uuid(), kind="edi", path="a.edi")
man.records[rec.rid] = rec
root: str#
version: int = 1#
records: dict[str, Record]#
to_dict()#

Return a nested mapping suitable for JSON or TOML.

A dict with root, version, and a nested records mapping of ids to plain dicts.

Return type:

dict[str, Any]

classmethod from_dict(d)#

Create a Manifest from a nested mapping.

Parameters:

d (dict) – Mapping previously produced by to_dict().

Returns:

New manifest with Record instances rebuilt.

Return type:

Manifest

class pycsamt.core.ManifestStore#

Bases: CoreObject

Interface for loading and saving Manifest objects.

Subclasses provide concrete persistence (e.g. JSON on disk, TOML, or a database). The interface is intentionally small.

Notes

Implementations should validate the manifest version and any schema expectations relevant to their storage.

Examples

A minimal JSON store sketch:

class JsonStore(ManifestStore):
    def load(self, path: Path) -> Manifest:
        data = json.loads(path.read_text())
        return Manifest.from_dict(data)

    def save(self, man: Manifest, path: Path) -> None:
        path.write_text(json.dumps(man.to_dict(), indent=2))

See also

Manifest, Record, json, tomllib

load(path)#
Parameters:

path (Path)

Return type:

Manifest

save(man, path)#
Parameters:
Return type:

None

class pycsamt.core.FileManifestStore#

Bases: ManifestStore

Disk-backed Manifest persistence utilities.

This store loads and saves manifests from files on disk. Reading supports JSON by default, and can parse TOML when tomllib is available. Saving is JSON-only in this implementation.

Behavior#

  • If the given path does not exist on load, a new empty Manifest is returned with root set to the parent directory of path.

  • Files are read and written with utf-8 encoding.

  • TOML saving is not implemented and raises RegistryError.

Notes

This class is intentionally minimal and does not implement atomic writes or file locking. For concurrent writers use a safer store or wrap calls with your own locking.

Examples

>>> from pathlib import Path
>>> from pycsamt.core._registry import (
...     FileManifestStore, Manifest)
>>> store = FileManifestStore()
>>> mpath = Path("manifest.json")
>>> man = store.load(mpath)
>>> isinstance(man, Manifest)
True
>>> store.save(man, mpath)

References

load(path)#

Load a Manifest from path.

Parameters:

path (pathlib.Path) – File to read. If it does not exist, return a new empty manifest with root set to path.parent.

Returns:

Deserialized manifest.

Return type:

Manifest

Raises:

Notes

When the suffix is .toml and tomllib is missing, the content is parsed as JSON, which will likely fail. Install tomllib (Python 3.11+) to read TOML files.

save(man, path)#

Serialize man to JSON at path using utf-8.

Parameters:
  • man (Manifest) – Manifest to write.

  • path (pathlib.Path) – Destination file. The suffix determines support.

Raises:
  • RegistryError – If a TOML file is requested (.toml suffix).

  • OSError – For write or permission errors.

Return type:

None

Notes

This method writes pretty-printed JSON (indent=2). TOML saving is not implemented here by design.

class pycsamt.core.Registry(root, *, manifest_name='manifest.json', store=None)#

Bases: CoreObject

High-level helper to manage a file-backed registry.

A Registry binds a filesystem root with a manifest file and provides convenience methods to add files or objects, query records, and persist changes.

Parameters:
  • root (path-like) – Base directory for data and the manifest file.

  • manifest_name (str, optional) – Manifest filename within root. Defaults to "manifest.json".

  • store (ManifestStore or None, optional) – Custom persistence backend. Defaults to FileManifestStore.

Variables:
  • root (pathlib.Path) – Absolute path to the registry root.

  • manifest_path (pathlib.Path) – Full path of the manifest file.

  • store (ManifestStore) – Persistence backend in use.

  • manifest (Manifest) – In-memory manifest loaded at construction.

Notes

On initialization, the manifest is loaded (or created if missing) and its root is set to the absolute root path. All relative paths passed later are resolved under root.

Examples

Create a registry and register an EDI file:

>>> from pycsamt.core._registry import Registry
>>> reg = Registry("data")
>>> rec = reg.add_path("site001.edi", kind="edi",
...                   fmt="edi")
>>> reg.get(rec.rid).kind
'edi'

Register a Python object and tag it:

>>> class Dummy:
...     station = "S01"
...     station_id = 1
>>> d = Dummy()
>>> rec = reg.add_obj(d, tags=["demo"])
>>> "demo" in rec.tags
True

Query and update metadata:

>>> regs = reg.list(kind="edi")
>>> found = reg.find(kind="edi")
>>> _ = reg.update_meta(rec.rid, site="S001")

References

save()#

Persist the in-memory manifest to manifest_path.

Notes

Delegates to store. Use this after batch updates if you have modified manifest directly.

Return type:

None

add_path(p, *, kind=None, fmt=None, dataid=None, station_id=None, tags=None, meta=None, with_hash=True)#

Add a file-based record into the registry.

Parameters:
  • p (path-like) – File path. If relative, it is resolved under root.

  • kind (str or None, optional) – Logical kind for the record. Defaults to "meta" when not provided.

  • fmt (str or None, optional) – Short format tag, such as "edi".

  • dataid (str or None, optional) – External dataset id or survey id.

  • station_id (str or int or None, optional) – Station identifier. Converted to string if provided.

  • tags (Iterable[str] or None, optional) – User-defined labels.

  • meta (dict or None, optional) – Extra JSON-serializable metadata.

  • with_hash (bool, optional) – If True and the file exists, compute SHA-256 and set Record.checksum. Defaults to True.

Returns:

The created record.

Return type:

Record

Notes

When the file does not exist, the checksum is left unset. The manifest is saved immediately after insertion.

Examples

>>> from pycsamt.core._registry import Registry
>>> reg = Registry("data")
>>> rec = reg.add_path("a.edi", kind="edi")
add_obj(obj, *, rid=None, kind=None, tags=None, meta=None, path=None)#

Add an object-backed record using heuristics for kind.

Parameters:
  • obj (Any) – Object to classify. guess_kind() is used when kind is not provided.

  • rid (str or None, optional) – Optional explicit record id. Defaults to a random id.

  • kind (str or None, optional) – Override classification if given.

  • tags (Iterable[str] or None, optional) – User-defined labels.

  • meta (dict or None, optional) – Extra JSON-serializable metadata.

  • path (str or pathlib.Path or None, optional) – Optional path reference to associate with the object.

Returns:

The created record.

Return type:

Record

Notes

If the object exposes attributes station or station_id, they are copied into the record when available. The manifest is saved immediately.

Examples

>>> from pycsamt.core._registry import Registry
>>> reg = Registry("data")
>>> class Obj:
...     station = "S02"; station_id = 2
>>> rec = reg.add_obj(Obj(), tags=["x"])
get(rid)#

Return the Record for rid or raise an error.

Parameters:

rid (str)

Return type:

Record

list(*, kind=None)#

List records, optionally filtering by kind.

Parameters:

kind (str or None, optional) – If provided, only records whose kind matches are returned.

Returns:

Records in insertion order as stored in the manifest.

Return type:

list of Record

find(*, tag=None, kind=None, dataid=None)#

Filter records by any combination of tag, kind, or dataid.

Parameters:
  • tag (str or None, optional) – Require that this tag is present in the record tags.

  • kind (str or None, optional) – Require that the record kind matches this value.

  • dataid (str or None, optional) – Require that the record data id matches this value.

Returns:

Records satisfying all provided predicates.

Return type:

list of Record

Examples

>>> from pycsamt.core._registry import Registry
>>> reg = Registry("data")
>>> reg.find(kind="edi")
update_meta(rid, **fields)#

Update meta fields of the record with id rid.

Parameters:
  • rid (str) – Record identifier.

  • **fields (Any) – Key-value pairs merged into Record.meta.

Returns:

The updated record.

Return type:

Record

Notes

Updates the updated timestamp and saves the manifest.

remove(rid)#

Remove the record with id rid and persist the manifest.

Parameters:

rid (str) – Record identifier.

Return type:

None

Notes

The removal is silent when the id is unknown. The manifest is saved after the operation.

pycsamt.core.guess_kind(obj)#

Best-effort classification for common MT/EM artefacts.

The function inspects obj.__class__.__module__ and class name to assign a coarse category usable by higher-level tools. It recognizes common terms such as "seg", "edi", "zonge", "avg", and "jones".

Parameters:

obj (Any) – Any Python object. Only its class name and module path are inspected. Iterables are handled specially.

Returns:

One of the following labels:

  • "edi" — an EDI item.

  • "edi_col" — an EDI collection.

  • "avg" — a Zonge AVG artefact.

  • "j" — a Jones J-file object.

  • "j_col" — a collection of J-files.

  • "list" — a list or tuple of items.

  • "meta" — a fallback for other metadata.

Return type:

str

Notes

This is heuristic and string-based. It is intentionally liberal to remain dependency-free. Extend or override at the call site if stricter typing is needed.

Examples

>>> class Dummy: ...
>>> guess_kind(Dummy())
'meta'
>>> guess_kind([])
'list'

See also

Record, Manifest

pycsamt.core.register_packer(kind, packer)#

Register a serializer/deserializer pair for a given kind.

Parameters:
  • kind (str) – Case-insensitive key that identifies the packer.

  • packer (tuple(callable, callable)) – Pair (pack, unpack). The first callable takes an object and returns a dict; the second takes that mapping and returns the reconstructed object.

Raises:

ValueError – If kind is empty or not a str.

Return type:

None

Notes

Registration is global to the current process via an internal dictionary. Re-registering the same kind will overwrite the previous pair.

Examples

>>> def pack(x): return {"v": x}
>>> def unpack(d): return d["v"]
>>> register_packer("toy", (pack, unpack))
>>> get_packer("toy") is not None
True

References

pycsamt.core.get_packer(kind)#

Return the registered packer for kind or None.

Parameters:

kind (str) – Case-insensitive key used at registration time.

Returns:

The (pack, unpack) pair if found, else None.

Return type:

tuple(callable, callable) or None

Examples

>>> _ = get_packer("missing") is None
>>> # After registering:
>>> # register_packer("toy", (pack, unpack))
>>> pk = get_packer("toy")
>>> callable(pk[0]) and callable(pk[1])
True
pycsamt.core.list_packers()#

List available packers as a mapping of kind to signatures.

Returns:

Mapping kind -> "pack_name | unpack_name" for quick inspection.

Return type:

dict[str, str]

Notes

Only names available via __name__ are shown. For callables without a name, repr is used.

Examples

>>> isinstance(list_packers(), dict)
True
pycsamt.core.pack_to_file(obj, path, *, kind='bundle')#

Serialize an object to .npz using a registered packer.

Parameters:
  • obj (Any) – Object to serialize. Must be supported by the chosen kind (i.e., its packer).

  • path (path-like) – Output file path. .npz is recommended.

  • kind (str, optional) – Packer name. Defaults to "bundle".

Returns:

The path actually written.

Return type:

pathlib.Path

Raises:
  • ValueError – If no packer is registered for kind.

  • OSError – For filesystem write errors.

Notes

Data are written with numpy.savez_compressed(), which stores arrays losslessly and compresses the archive.

Examples

Pack a bundle to disk:

>>> # Assume a TFBundle instance: bndl
>>> out = pack_to_file(bndl, "site001.npz", kind="bundle")
>>> out.name.endswith(".npz")
True

References

pycsamt.core.unpack_from_file(path, *, kind='bundle')#

Deserialize an object from .npz using a registered packer.

Parameters:
  • path (path-like) – Input NPZ path created by pack_to_file().

  • kind (str, optional) – Packer name. Defaults to "bundle". Must match the packer used during serialization.

Returns:

The reconstructed object.

Return type:

Any

Raises:
  • ValueError – If no packer is registered for kind.

  • OSError – For filesystem read errors.

  • KeyError – If expected payload keys are missing for the packer.

Notes

Loading uses allow_pickle=False for safety; payloads must be composed of basic NumPy/JSON types.

Examples

Load a previously saved bundle:

>>> obj = unpack_from_file("site001.npz", kind="bundle")
>>> hasattr(obj, "z") and hasattr(obj, "freq")
True
class pycsamt.core.RegistryAPI(root, *, manifest_name='manifest.json')#

Bases: CoreObject

Convenience façade over Registry.

This high-level helper wires a registry root, ensures a packs/ folder exists under that root, and exposes both thin proxies to low-level registry calls and ergonomic helpers for packing objects to NPZ bundles and restoring them later.

Parameters:
  • root (path-like) – Base directory that will contain the manifest and, if needed, a packs/ subdirectory for NPZ bundles.

  • manifest_name (str, optional) – Manifest filename within root. Defaults to "manifest.json".

Variables:
  • low (Registry) – The underlying registry instance that performs the core operations (load/save, add, query).

  • root (pathlib.Path) – Absolute path to the registry root directory.

  • pack_dir (pathlib.Path) – Directory where NPZ bundles are stored (root/packs).

Notes

RegistryAPI aims to be pragmatic and file-system first. It does not do locking or concurrency control. For multi- process scenarios, add your own synchronization.

The packing flow defaults to the "bundle" packer, which serializes TFBundle-compatible objects into NPZ using numpy.savez_compressed().

Examples

Create an API, add an EDI file, and list records:

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> _ = api.add_file("site001.edi", kind="edi", fmt="edi")
>>> [r.kind for r in api.list(kind="edi")]
['edi']

Pack an object to an NPZ bundle and store it in packs:

>>> class Dummy:
...     station = "S01"; station_id = 1
>>> rec = api.add_object(Dummy(), pack=True,
...                      pack_name="S01.npz")
>>> rec.kind
'bundle'

Materialize a record back into a Python object:

>>> obj = api.materialize(rec.rid)  # may be Dummy or path

Convert a record to an EDI representation:

>>> edi_like = api.to_edi(rec.rid)

References

save()#

Persist the manifest to disk.

Thin proxy to Registry.save(). Use after mutating the underlying manifest directly, though typical helpers call save for you.

Return type:

None

list(*, kind=None)#

List all records, optionally filtered by kind.

Parameters:

kind (str or None, optional) – If provided, only records whose kind matches are returned.

Returns:

Shallow views of stored Record objects.

Return type:

list of Record

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> isinstance(api.list(), list)
True
get(rid)#

Return the record by id or raise an error.

Parameters:

rid (str) – Record identifier returned at insertion time.

Returns:

The matching record.

Return type:

Record

Raises:

RegistryError – If the id does not exist in the manifest.

find(*, tag=None, kind=None, dataid=None)#

Filter records by tag, kind, and/or data id.

Parameters:
  • tag (str or None, optional) – Require that the tag is present in Record.tags.

  • kind (str or None, optional) – Require that the record kind matches the value.

  • dataid (str or None, optional) – Require equality with Record.dataid.

Returns:

Records satisfying all given predicates.

Return type:

list of Record

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> api.find(kind="edi")
[...]  # list of records
add_file(path, *, kind=None, fmt=None, dataid=None, station_id=None, tags=None, meta=None, with_hash=True)#

Register a file path as a new record.

Parameters:
  • path (path-like) – File path. Relative paths are resolved under root.

  • kind (str or None, optional) – Logical kind (e.g. "edi", "avg"). If omitted, defaults to "meta".

  • fmt (str or None, optional) – Short format hint (e.g. "edi").

  • dataid (str or None, optional) – External data or survey id.

  • station_id (str or int or None, optional) – Station identifier. Cast to string if provided.

  • tags (list[str] or None, optional) – User-defined labels to ease later queries.

  • meta (dict[str, Any] or None, optional) – Extra JSON-serializable metadata.

  • with_hash (bool, optional) – If True and the file exists, compute and store a SHA-256 checksum. Defaults to True.

Returns:

The created and persisted record.

Return type:

Record

Notes

This method saves the manifest on success.

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> rec = api.add_file("site001.edi",
...                    kind="edi", fmt="edi")
>>> rec.kind, rec.fmt
('edi', 'edi')
add_object(obj, *, kind=None, tags=None, meta=None, pack=False, pack_name=None)#

Register an object and, optionally, pack it to an NPZ bundle.

Parameters:
  • obj (Any) – Object to register. If kind is None, a coarse label is inferred via guess_kind().

  • kind (str or None, optional) – Explicit kind override (e.g. "edi", "bundle").

  • tags (list[str] or None, optional) – User-defined labels.

  • meta (dict[str, Any] or None, optional) – Extra JSON-serializable metadata for the record.

  • pack (bool, optional) – If True, serialize obj with the "bundle" packer into pack_dir and rewrite the record to refer to that NPZ on disk. Defaults to False.

  • pack_name (str or None, optional) – Output NPZ filename when pack=True. Defaults to "<rid>.npz".

Returns:

The created record. When pack=True, the record kind becomes "bundle" and path points to the NPZ file inside pack_dir.

Return type:

Record

Notes

Packing uses pack_to_file() with kind="bundle". The manifest is saved after packing.

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> class Obj: station="S02"; station_id=2
>>> rec = api.add_object(Obj(), tags=["demo"])
>>> isinstance(rec.rid, str)
True

Pack immediately to NPZ:

>>> rec2 = api.add_object(Obj(), pack=True,
...                       pack_name="S02.npz")
>>> rec2.kind
'bundle'

See also

pycsamt.core.registry.pack_to_file, pycsamt.core.registry.guess_kind

materialize(rid)#

Load a record into a concrete Python object when possible.

Parameters:

rid (str) – Record id to resolve.

Returns:

If the record is a "bundle" with a valid path, returns the unpacked object via unpack_from_file(). For known kinds with readable files this returns:

Otherwise, returns the string path (or None) of the record.

Return type:

Any

Notes

Errors during import or read are swallowed and the path is returned instead. This keeps the method side-effect free with respect to the registry while remaining robust.

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> # Assuming a valid record id:
>>> # obj = api.materialize(rid)
to_edi(rid, **kw)#

Convert a record’s materialized object into an EDI-like form.

Parameters:
  • rid (str) – Record id to resolve and convert.

  • **kw (Any) – Forwarded to to_edi(), which performs the actual conversion.

Returns:

An object produced by to_edi(). The concrete type depends on the available converters and inputs.

Return type:

Any

Notes

This is a convenience wrapper that first calls materialize(), then delegates to pycsamt.core.base.to_edi().

Examples

>>> from pycsamt.core.registry import RegistryAPI
>>> api = RegistryAPI("data")
>>> # Convert an existing record to an EDI representation
>>> edi_like = api.to_edi("some-record-id")
pycsamt.core.bundle_from_edi(obj)#

Build a TFBundle from an EDI-like object.

The function probes common attribute names found in EDI objects and populates a neutral bundle. It tolerates missing fields and does not enforce array types.

Parameters:

obj (Any) – EDI-like object. Attributes such as freq, z, z_err, tipper, rho, phase, station, lat, lon, elev, azimuth are searched with several conventional aliases.

Returns:

A bundle with fields filled where data could be found.

Return type:

TFBundle

Notes

If the tipper has shape (n, 1, 2) it is normalized to (n, 2). Unknown shapes are left unchanged.

Examples

>>> from pycsamt.core.mixins import bundle_from_edi
>>> class E:
...     def __init__(self):
...         self.freq = [1.0, 2.0]
...         self.phase = [45.0, 50.0]
...         self.rho = [100.0, 120.0]
...
>>> b = bundle_from_edi(E())
>>> b.freq, b.phase[0]
([1.0, 2.0], 45.0)
class pycsamt.core.BundleMixin#

Bases: CoreObject

Mixin for single items convertible to and from bundles.

Subclasses implement to_bundle and from_bundle to map between their internal representation and the neutral TFBundle.

Notes

to_edi delegates to pycsamt.core.base.to_edi(), which uses the adapter registry to pick the right converter.

Examples

>>> from pycsamt.core.mixins import BundleMixin
>>> from pycsamt.core.base import TFBundle
>>>
>>> class Item(BundleMixin):
...     def __init__(self, b): self._b = b
...     def to_bundle(self): return self._b
...     @classmethod
...     def from_bundle(cls, b): return cls(b)
...
>>> b = TFBundle(freq=[1.0], rho=[100.], phase=[45.])
>>> it = Item.from_bundle(b)
>>> isinstance(it.to_bundle(), TFBundle)
True
to_bundle()#

Return a TFBundle representation. Must be implemented by subclasses.

Return type:

TFBundle

classmethod from_bundle(bundle)#

Construct an instance from a TFBundle.

Returns a new instance of the implementing class.

Must be implemented by subclasses.

Parameters:

bundle (TFBundle)

static ensure_station_name(name, station_id)#

Validate or synthesize a station name via policy.

Parameters:
  • name (str or None) – Preferred station/site name if available.

  • station_id (str, int or None) – Identifier used to synthesize a name when needed.

Returns:

Validated or synthetic name.

Return type:

str

See also

pycsamt.core.base.ensure_station

Underlying helper with policy lookup.

to_edi(*, key=None, **k)#

Convert self to EDI or EDICollection via dispatch.

Parameters:
  • key (str, optional) – Adapter key override (e.g. 'avg', 'j').

  • **k (Any) – Extra keyword arguments forwarded to the adapter.

Returns:

EDI object or EDICollection, depending on adapter.

Return type:

Any

Notes

Dispatch is handled by pycsamt.core.base.to_edi(). A suitable adapter must have been registered, otherwise a runtime error is raised.

classmethod from_edi(edi_obj)#

Construct instance(s) from an EDI object or a collection.

If edi_obj looks like a collection, every element is converted to a TFBundle and fed to from_bundle. Otherwise the single object is converted and fed to from_bundle.

Parameters:

edi_obj (Any) – EDI object or an iterable of EDI objects.

Returns:

Instance or list of instances of the implementing class.

Return type:

Any

Examples

>>> from pycsamt.core.mixins import BundleMixin, bundle_from_edi
>>> class I(BundleMixin):
...     def __init__(self, b): self._b = b
...     def to_bundle(self): return self._b
...     @classmethod
...     def from_bundle(cls, b): return cls(b)
...
>>> e = type('E', (), {'freq':[1.0], 'rho':[100.], 'phase':[45.]})()
>>> obj = I.from_edi(e)
class pycsamt.core.BundleContainerMixin#

Bases: BundleMixin

Mixin for containers of items that implement to_bundle.

This mixin provides iteration over bundles and a convenience method to convert all items to an EDI collection using the adapter dispatch.

Notes

Detection of children is permissive: if the container has an items() method, values are inspected; otherwise the container is iterated directly.

Examples

>>> from pycsamt.core.mixins import BundleContainerMixin
>>> from pycsamt.core.base import TFBundle
>>>
>>> class Bag(dict, BundleContainerMixin):
...     pass
...
>>> _ = Bag({0: type('X',(),{'to_bundle':lambda s: TFBundle()})()})
>>> list(_.iter_bundles())[0].__class__.__name__
'TFBundle'
iter_bundles()#

Yield contained items as TFBundle objects.

Return type:

Iterator[TFBundle]

to_edi_collection(*, key=None, **k)#

Convert all child bundles through adapter dispatch.

Parameters:
  • key (str, optional) – Adapter key override for all children.

  • **k (Any) – Extra keyword arguments forwarded to the adapter.

Returns:

A list of EDI objects. Callers may wrap this into an actual EDICollection.

Return type:

list of Any

Examples

>>> from pycsamt.core.mixins import BundleContainerMixin
>>> # Requires registered adapters to actually run:
>>> # objs = MyContainer(...).to_edi_collection()
class pycsamt.core.StationNamePolicy(allow_pattern='A-Za-z0-9_\\-', maxlen=32, prefix='S', pad=3, strip=True, custom_normalize=<function StationNamePolicy.<lambda>>)#

Bases: object

Rules to validate and synthesize station names.

This policy is applied during inter-format conversion (AVG or Jones -> EDI). When a provided name is missing or invalid, a synthetic name is derived from the station id.

Parameters:
  • allow_pattern (str, optional) – Regex character class (without brackets) that defines the whitelist of allowed characters. The default accepts ASCII letters, digits, underscore and dash.

  • maxlen (int, optional) – Maximum length of a station name after normalization.

  • prefix (str, optional) – Prefix used when creating synthetic names.

  • pad (int, optional) – Zero-padding width for numeric station ids.

  • strip (bool, optional) – If True, strip leading and trailing whitespace first.

  • custom_normalize (Callable[[str], str], optional) – Hook called before validation. Can perform additional transliteration or case normalization.

Notes

Normalization runs as: strip → custom_normalize → filter by allow_pattern → truncate to maxlen. Empty results are treated as invalid.

Examples

>>> from pycsamt.core.config import StationNamePolicy
>>> pol = StationNamePolicy(prefix=\"S\", pad=4)
>>> pol.ensure(\" Site-1  \", station_id=None)
'Site-1'
>>> pol.ensure(None, station_id=7)
'S0007'
allow_pattern: str = 'A-Za-z0-9_\\-'#
maxlen: int = 32#
prefix: str = 'S'#
pad: int = 3#
strip: bool = True#
static custom_normalize(s)#
validate(name)#

Validate and normalize a name.

Parameters:

name (str or None) – Candidate station name.

Returns:

Normalized name if valid; otherwise None.

Return type:

str or None

Notes

The method does not synthesize names. Use ensure for a name-or-fallback behavior.

synthesize(station_id)#

Create a deterministic synthetic name from a station id.

Parameters:

station_id (Any or None) – Station identifier. Numeric ids are zero-padded. Non- numeric ids are compacted by removing non-word chars.

Returns:

Synthetic station name.

Return type:

str

Examples

>>> StationNamePolicy().synthesize(12)
'S012'
>>> StationNamePolicy(prefix='X').synthesize('AB-01')
'AB01'
ensure(name, station_id)#

Return a valid station name, validating or synthesizing.

Parameters:
  • name (str or None) – Provided station name.

  • station_id (Any or None) – Station identifier used if name is invalid.

Returns:

Validated name or a synthetic fallback.

Return type:

str

See also

validate

Validate only, without fallback.

synthesize

Create a name from the station id only.

class pycsamt.core.CoreConfig(empty=1e+32, strict=False, case_sensitive_sections=False, on_duplicate_station='replace', target_format='edi', log_level='WARNING', freq_order='desc', freq_tol=1e-09, compute_res_from_z=True, compute_z_from_res=True, load_spectra=True, load_time_series=False, station_policy=<factory>, error_fill_value=nan, infer_errors=True, encoding='utf-8', newline='\n', backend=<factory>)#

Bases: object

Global configuration container for pycsamt.

The object stores defaults for parsing, conversion, and population of EDI data structures. Use configure() for updates or config_context() for temporary overrides.

Parameters:
  • empty (float) – Sentinel used in legacy files to mark missing values.

  • strict (bool) – If True, raise on recoverable parse issues; otherwise, degrade gracefully when possible.

  • case_sensitive_sections (bool) – Treat section names as case-sensitive when reading.

  • on_duplicate_station ({'replace', 'keep', 'error'}) – Policy when collection loaders encounter duplicate station ids.

  • target_format ({'edi'}) – Preferred internal representation for processing. Keep as ‘edi’ to normalize all inputs to EDI objects.

  • log_level (str) – Default logging level for package loggers.

  • freq_order ({'asc', 'desc'}) – Preferred order for frequency vectors after normalization.

  • freq_tol (float) – Relative tolerance for de-duplicating near-equal frequencies.

  • compute_res_from_z (bool) – Compute (rho, phase) from Z when absent.

  • compute_z_from_res (bool) – Reconstruct Z from (rho, phase) when Z is missing.

  • load_spectra (bool) – If True, preserve spectra sections when available.

  • load_time_series (bool) – If True, preserve time-series sections when available.

  • station_policy (StationNamePolicy) – Validation and synthesis rules for station names.

  • error_fill_value (float) – Fill value used when error arrays are required by a consumer but not available.

  • infer_errors (bool) – If True, allow simple heuristics to infer missing errors from weights or coherency measures when present.

  • encoding (str) – Default text encoding for reading legacy files.

  • newline (str) – Line terminator to use when writing files.

  • backend (dict) – Reserved space for per-backend knobs.

Notes

The instance stored in this module is mutable by design. Use configure() or config_context() instead of assigning attributes directly, so validation is applied.

Examples

>>> from pycsamt.core.config import configure, get_config
>>> _ = configure(freq_order='desc', strict=False)
>>> get_config().freq_order
'desc'
empty: float = 1e+32#
strict: bool = False#
case_sensitive_sections: bool = False#
on_duplicate_station: str = 'replace'#
target_format: str = 'edi'#
log_level: str = 'WARNING'#
freq_order: str = 'desc'#
freq_tol: float = 1e-09#
compute_res_from_z: bool = True#
compute_z_from_res: bool = True#
load_spectra: bool = True#
load_time_series: bool = False#
station_policy: StationNamePolicy#
error_fill_value: float = nan#
infer_errors: bool = True#
encoding: str = 'utf-8'#
newline: str = '\n'#
backend: dict[str, Any]#
copy()#

Return a deep copy of the configuration.

Returns:

A detached copy that can be safely mutated.

Return type:

CoreConfig

Notes

Used internally by config_context() to restore the previous state atomically.

pycsamt.core.get_config()#

Return the live CoreConfig singleton.

Returns:

The active configuration object.

Return type:

CoreConfig

Notes

The returned instance is mutable. Prefer configure() or config_context() for controlled updates.

pycsamt.core.configure(**kwargs)#

Update configuration fields with validation.

Parameters:

**kwargs (Any) – Field names and values to set on the global config.

Returns:

The updated configuration object.

Return type:

CoreConfig

Raises:

Notes

Also aligns the package logger level to log_level, if that logger has been created.

Examples

>>> from pycsamt.core.config import configure
>>> _ = configure(freq_order='asc', infer_errors=False)
pycsamt.core.reset_config()#

Reset the global configuration to factory defaults.

Notes

A new CoreConfig instance replaces the existing singleton. Any references to the previous instance will not reflect subsequent changes.

Return type:

None

pycsamt.core.config_context(**overrides)#

Context manager for temporary configuration overrides.

Parameters:

**overrides (Any) – Field names and temporary values.

Yields:

CoreConfig – The active configuration during the context.

Return type:

Iterator[CoreConfig]

Notes

On exit, the previous configuration is restored atomically, even if an exception is raised.

Examples

>>> from pycsamt.core.config import config_context, get_config
>>> with config_context(strict=True):
...     assert get_config().strict is True
>>> assert get_config().strict is False
pycsamt.core.to_dict()#

Serialize the current configuration to a plain dict.

Returns:

A JSON-serializable mapping of all fields.

Return type:

dict

Examples

>>> from pycsamt.core.config import to_dict
>>> d = to_dict()
>>> 'freq_order' in d
True
pycsamt.core.register_adapter(key, factory)#

Register a format adapter that yields an EDI object or collection.

Parameters:
  • key (str) – Format key, e.g. 'avg' or 'j'.

  • factory (Callable[..., Any]) – Callable that accepts a source object and returns an EDI object or an EDICollection.

Raises:

ValueError – If key is not a non-empty string.

Return type:

None

Notes

Adapters are resolved lazily at call time, which avoids heavy imports and circular dependencies.

See also

get_adapter

Retrieve a registered adapter.

list_adapters

Inspect the registry.

pycsamt.core.get_adapter(key)#

Return the adapter factory for a key.

Parameters:

key (str) – Format key used during registration.

Returns:

The factory callable if found, else None.

Return type:

Callable or None

pycsamt.core.list_adapters()#

List registered adapters with readable names.

Returns:

Mapping {key: 'qualname'} for registered factories.

Return type:

dict

Examples

>>> from pycsamt.core.config import list_adapters
>>> isinstance(list_adapters(), dict)
True

Core Modules#

pycsamt.core.base

Foundational objects and transfer-function containers for pyCSAMT.

pycsamt.core.config

Runtime configuration, adapter registration, and configuration contexts.

pycsamt.core.mixins

Reusable mixins for creating and manipulating transfer-function bundles.

pycsamt.core.registry

Public packer registry and serialization helpers for survey objects.

Transformer Base#

class pycsamt.core.TransformerMixin#

Bases: MTBase

Template utilities for AVG/J -> EDI transformations.

This module defines TransformerMixin, a light template that standardizes the steps needed to convert foreign transfer-function containers (e.g. Zonge AVG or Jones J) into EDI files or collections.

The mixin focuses on finalization of a neutral payload (pycsamt.core.base.TFBundle): validating the station name, ordering and de-duplicating frequencies, and optionally filling missing pieces (Z vs rho/phase) according to user config.

Notes

Subclasses provide the data plumbing:

The default hooks for filling missing parts are no-ops. Backends can override TransformerMixin.compute_res_from_z() and TransformerMixin.compute_z_from_res().

Subclasses implement the data path while this mixin handles standard finalization steps on a TFBundle:

  1. Validate or synthesize the station name using the global policy.

  2. Order frequencies per config (asc/desc).

  3. De-duplicate nearly equal frequencies using a relative tolerance.

  4. Optionally fill missing parts (Z vs rho/phase) via overridable hooks.

Finalization obeys the global configuration from pycsamt.core.config. In particular:

  • freq_order controls sorting.

  • freq_tol controls de-duplication.

  • compute_res_from_z and compute_z_from_res toggle filling behavior.

See also

pycsamt.core.transformers.AVGtoEDI

Concrete AVG → EDI/EDICollection converter.

pycsamt.core.transformers.JtoEDI

Concrete J → EDI/EDICollection converter.

pycsamt.core.base.TFBundle

Neutral payload used across backends.

pycsamt.core.config

Global configuration and frequency policies.

Examples

A minimal custom transformer:

>>> from pycsamt.core._transformers import TransformerMixin
>>> from pycsamt.core.base import TFBundle
>>>
>>> class MyX(TransformerMixin):
...     def extract(self, source):
...         # pull TF data from source into a bundle
...         return TFBundle(freq=[1.0], rho=[100.], phase=[45.])
...     def emit_edi(self, bundle):
...         # return an EDI-like stub for illustration
...         return {'freq': bundle.freq, 'rho': bundle.rho}
...
>>> out = MyX().transform(object())
>>> isinstance(out, dict)
True

References

extract(source)#

Extract a TFBundle from source.

This is the ingest step. Implementors parse the foreign object (or file path) and return a neutral bundle. Keep the method free of side effects.

Parameters:

source (Any) – Backend-specific object or path.

Returns:

A bundle containing frequencies and transfer functions.

Return type:

TFBundle

Raises:

NotImplementedError – Must be provided by subclasses.

Notes

Prefer leaving station naming and frequency manipulation to the mixin. Provide raw content here.

emit_edi(bundle)#

Materialize an EDI object from a finalized bundle.

This is the emit step. Implementors build and return an EDI object (or a compatible stub).

Must be provided by subclasses.

Notes

Do not attempt to reorder frequencies here. The bundle has already been finalized by _finalize().

Parameters:

bundle (TFBundle)

Return type:

Any

post_emit(edi_obj, source, bundle)#

Optional last-mile adjustments after EDI creation.

Use this hook to enrich the EDI object with auxiliary metadata (e.g. site location, elevation) that is awkward to thread through earlier steps.

Parameters:
  • edi_obj (Any) – The EDI object returned by emit_edi().

  • source (Any) – The original source object used in extract().

  • bundle (TFBundle) – The finalized bundle.

Returns:

The potentially modified EDI object.

Return type:

Any

Notes

The default implementation returns edi_obj unmodified.

compute_res_from_z(b)#

Compute apparent resistivity/phase from Z.

This hook is called by _fill_missing() when compute_res_from_z is True in the global config, and the bundle holds Z but lacks (rho, phase).

Parameters:

b (TFBundle) – Input bundle.

Returns:

The updated bundle (may be the same instance).

Return type:

TFBundle

Notes

The default implementation is a no-op. Subclasses may override to apply the standard MT relations [1]_ [2]_.

compute_z_from_res(b)#

Reconstruct Z from apparent resistivity/phase.

This hook is called by _fill_missing() when compute_z_from_res is True in the global config, and the bundle lacks Z but holds (rho, phase).

Parameters:

b (TFBundle) – Input bundle.

Returns:

The updated bundle (may be the same instance).

Return type:

TFBundle

Notes

The default implementation is a no-op. Subclasses may override to apply physically consistent reconstruction.

transform(source, *, name=None, station_id=None)#

One-shot transform: extract → finalize → emit → post.

This orchestrates the whole conversion pipeline:

  1. extract() to build a TFBundle.

  2. _finalize() to normalize the bundle.

  3. emit_edi() to create the EDI object.

  4. post_emit() for optional enrichment.

Parameters:
  • source (Any) – Source object or file path.

  • name (str, optional) – Preferred station name override.

  • station_id (str or int, optional) – Identifier used if a name must be synthesized.

Returns:

The resulting EDI object (or compatible subtype).

Return type:

Any

Examples

>>> from pycsamt.core._transformers import TransformerMixin
>>> class Mini(TransformerMixin):
...     def extract(self, s):
...         from pycsamt.core.base import TFBundle
...         return TFBundle(freq=[1.], rho=[100.], phase=[45.])
...     def emit_edi(self, b):
...         return {'ok': True, 'n': len(b.freq)}
...
>>> Mini().transform(object())['ok']
True