pycsamt.ai.data.contracts#
Canonical, dependency-light data contracts for MT/AMT surveys.
The classes in this module define the boundary between survey ingestion and all downstream AI or physics code. They deliberately depend only on NumPy: EDI readers, interpolation policies, neural-network frameworks, and Maxwell solver backends belong elsewhere.
The canonical impedance axis order is (station, frequency, component).
Keeping this order explicit prevents a common class of scientifically quiet
errors in which stations, tensor components, or frequency order are swapped
without changing an array’s rank.
Functions
|
Concatenate compatible surveys along the station axis. |
Classes
|
Describe the sign, units, and rotation convention of impedance data. |
|
Summarize the usable fraction of a survey along each data axis. |
|
Validated MT/AMT observations on a common survey grid. |
- class pycsamt.ai.data.contracts.ImpedanceConvention(time_dependence='exp(+iwt)', units='V/A', rotation_deg=0.0, coordinate_orientation='x_north_y_east')[source]
Bases:
objectDescribe the sign, units, and rotation convention of impedance data.
- Parameters:
time_dependence ({"exp(+iwt)", "exp(-iwt)"}, default="exp(+iwt)") – Fourier time convention used for the stored complex impedance. Solver predictions must use the same convention before residuals are formed.
units ({"V/A"}, default="V/A") – Physical unit of electric field divided by magnetic field. The current canonical contract accepts SI impedance only.
rotation_deg (float, default=0.0) – Clockwise rotation already applied to the horizontal tensor axes, in degrees. Values are normalized to the interval
[0, 360).coordinate_orientation (str, default="x_north_y_east") – Human-readable definition of the horizontal tensor axes.
Examples
Record a tensor rotated clockwise into a geological strike frame:
>>> convention = ImpedanceConvention(rotation_deg=32.0) >>> convention.rotation_deg 32.0 >>> convention.to_dict()["time_dependence"] 'exp(+iwt)'
Notes
This object records a convention; it does not rotate or conjugate data. Such transformations must be explicit preprocessing operations that create a new
SurveyDataobject with updated provenance.- time_dependence: str = 'exp(+iwt)'
- units: str = 'V/A'
- rotation_deg: float = 0.0
- coordinate_orientation: str = 'x_north_y_east'
- to_dict()[source]
Return a JSON-serializable representation.
- Returns:
Convention fields with a schema discriminator.
- Return type:
Examples
>>> state = ImpedanceConvention().to_dict() >>> state["schema_version"], state["units"] (1, 'V/A')
- classmethod from_dict(data)[source]
Restore and validate a serialized convention.
- Parameters:
data (mapping) – State previously returned by
to_dict().- Returns:
Validated immutable convention.
- Return type:
- Raises:
ValueError – If the schema version is unsupported or a field is invalid.
Examples
>>> state = ImpedanceConvention(rotation_deg=15).to_dict() >>> ImpedanceConvention.from_dict(state).rotation_deg 15.0
- class pycsamt.ai.data.contracts.SurveyCoverage(overall, by_station, by_frequency, by_component, tipper_overall=None)[source]
Bases:
objectSummarize the usable fraction of a survey along each data axis.
- Parameters:
overall (float) – Fraction of valid impedance observations across the complete cube.
by_station (ndarray, shape (n_station,)) – Valid fraction for each station.
by_frequency (ndarray, shape (n_frequency,)) – Valid fraction for each frequency.
by_component (ndarray, shape (n_component,)) – Valid fraction for each impedance component.
tipper_overall (float or None, optional) – Fraction of valid tipper observations, or
Nonewhen absent.
Examples
Coverage is normally obtained from
SurveyData.coverage():>>> coverage = SurveyCoverage(1.0, [1.0], [1.0], [1.0]) >>> coverage.complete True
- overall: float
- by_station: ndarray
- by_frequency: ndarray
- by_component: ndarray
- class pycsamt.ai.data.contracts.SurveyData(impedance, frequencies_hz, station_names, components, coordinates_m, impedance_error=None, valid=None, tipper=None, tipper_error=None, tipper_valid=None, crs=None, metadata=<factory>, convention=<factory>)[source]
Bases:
objectValidated MT/AMT observations on a common survey grid.
- Parameters:
impedance (array-like of complex, shape (n_station, n_frequency, n_component)) – Complex impedance in V/A. Invalid entries may be NaN but must be false in
validafter construction.frequencies_hz (array-like, shape (n_frequency,)) – Positive, finite, unique, strictly monotonic frequencies.
station_names (sequence of str) – Names corresponding exactly to the station and component axes.
components (sequence of str) – Names corresponding exactly to the station and component axes.
coordinates_m (array-like, shape (n_station, 2 or 3)) – Projected x/y coordinates and optional elevation in metres. A missing third column is represented by NaN elevation.
impedance_error (array-like, optional) – Positive absolute standard errors with the same shape as impedance. Entries without usable errors are invalidated.
valid (array-like of bool, optional) – Explicit observation mask. It is combined with finite-value and error checks; invalid data are never silently imputed.
tipper (array-like, optional) – Optional complex magnetic transfer functions shaped
(n_station, n_frequency, 2)for Tx and Ty.tipper_error (array-like, optional) – Optional complex magnetic transfer functions shaped
(n_station, n_frequency, 2)for Tx and Ty.tipper_valid (array-like, optional) – Optional complex magnetic transfer functions shaped
(n_station, n_frequency, 2)for Tx and Ty.crs (str, optional) – Coordinate reference system identifier. Projected coordinates should normally provide an EPSG or WKT identifier.
metadata (mapping, optional) – Finite JSON-serializable provenance only.
convention (ImpedanceConvention, optional) – Explicit complex sign, SI unit, tensor-axis, and rotation convention.
- Variables:
impedance (ndarray) – Read-only complex impedance cube.
valid (ndarray of bool) – Read-only authoritative mask for usable impedance observations.
Examples
Construct a two-station survey with descending frequency order. Two- column coordinates are accepted and expanded with unknown elevations:
>>> z = np.ones((2, 3, 2), dtype=complex) * (1 + 2j) >>> survey = SurveyData( ... impedance=z, ... frequencies_hz=[100.0, 10.0, 1.0], ... station_names=["S01", "S02"], ... components=["xy", "yx"], ... coordinates_m=[[0.0, 0.0], [100.0, 0.0]], ... crs="EPSG:32630", ... ) >>> survey.shape (2, 3, 2) >>> survey.frequency_order 'descending'
Notes
Construction copies all numerical inputs and marks them read-only. The dataclass is therefore safe to share between training, validation, and reporting code without accidental in-place mutation.
- impedance: ndarray
- frequencies_hz: ndarray
- coordinates_m: ndarray
- convention: ImpedanceConvention
- property shape: tuple[int, int, int][source]
Return the canonical impedance shape.
Examples
>>> z = np.ones((1, 2, 1), dtype=complex) >>> s = SurveyData(z, [10, 1], ["S"], ["xy"], [[0, 0]]) >>> s.shape (1, 2, 1)
- property n_valid: int[source]
Return the number of usable impedance observations.
- Returns:
Count of
Trueentries invalid.- Return type:
Examples
>>> z = np.array([[[1 + 1j], [complex(np.nan, np.nan)]]]) >>> s = SurveyData(z, [10, 1], ["S"], ["xy"], [[0, 0]]) >>> s.n_valid 1
- property n_stations: int[source]
Return the number of stations.
- Returns:
Length of the station axis.
- Return type:
Examples
>>> s = SurveyData( ... np.ones((2, 1, 1), complex), ... [1], ... ["a", "b"], ... ["xy"], ... [[0, 0], [1, 0]], ... ) >>> s.n_stations 2
- property n_frequencies: int[source]
Return the number of frequencies.
- Returns:
Length of the frequency axis.
- Return type:
Examples
>>> s = SurveyData( ... np.ones((1, 2, 1), complex), [10, 1], ["S"], ["xy"], [[0, 0]] ... ) >>> s.n_frequencies 2
- property n_components: int[source]
Return the number of impedance components.
- Returns:
Length of the component axis.
- Return type:
Examples
>>> s = SurveyData( ... np.ones((1, 1, 2), complex), [1], ["S"], ["xy", "yx"], [[0, 0]] ... ) >>> s.n_components 2
- property frequency_order: str[source]
Return the monotonic direction of the frequency axis.
- Returns:
Direction in which frequencies are stored.
- Return type:
{“ascending”, “descending”}
Examples
>>> s = SurveyData( ... np.ones((1, 2, 1), complex), [1, 10], ["S"], ["xy"], [[0, 0]] ... ) >>> s.frequency_order 'ascending'
- property has_tipper: bool[source]
Whether optional Tx/Ty transfer functions are present.
- Returns:
Truewhentipperis populated.- Return type:
Examples
>>> s = SurveyData( ... np.ones((1, 1, 1), complex), [1], ["S"], ["xy"], [[0, 0]] ... ) >>> s.has_tipper False
- station_index(name)[source]
Return the integer position of a named station.
- Parameters:
name (str) – Exact, case-sensitive station identifier.
- Returns:
Position along the station axis.
- Return type:
- Raises:
KeyError – If
namedoes not occur instation_names.
Examples
>>> s = SurveyData( ... np.ones((2, 1, 1), complex), ... [1], ... ["A", "B"], ... ["xy"], ... [[0, 0], [1, 0]], ... ) >>> s.station_index("B") 1
- component_index(name)[source]
Return the integer position of an impedance component.
- Parameters:
name (str) – Exact, case-sensitive component name such as
"xy".- Returns:
Position along the component axis.
- Return type:
- Raises:
KeyError – If
nameis not stored.
Examples
>>> s = SurveyData( ... np.ones((1, 1, 2), complex), [1], ["S"], ["xy", "yx"], [[0, 0]] ... ) >>> s.component_index("yx") 1
- coverage()[source]
Calculate valid-data coverage along every impedance axis.
- Returns:
Overall, station, frequency, and component fractions. Optional tipper coverage is included when tipper data exist.
- Return type:
Examples
>>> z = np.ones((2, 2, 1), dtype=complex) >>> mask = np.array([[[True], [False]], [[True], [True]]]) >>> s = SurveyData( ... z, [10, 1], ["A", "B"], ["xy"], [[0, 0], [1, 0]], valid=mask ... ) >>> s.coverage().overall 0.75 >>> s.coverage().by_station.tolist() [0.5, 1.0]
- component_data(name)[source]
Return values, errors, and validity mask for one component.
- Parameters:
name (str) – Exact component name.
- Returns:
values (ndarray, shape (n_station, n_frequency)) – Read-only complex impedance view.
errors (ndarray or None) – Read-only absolute standard-error view, when available.
valid (ndarray of bool) – Read-only validity-mask view.
- Raises:
KeyError – If the named component is unavailable.
- Return type:
Examples
>>> z = np.ones((1, 2, 2), dtype=complex) >>> s = SurveyData(z, [10, 1], ["S"], ["xy", "yx"], [[0, 0]]) >>> values, errors, valid = s.component_data("xy") >>> values.shape, errors, valid.all() ((1, 2), None, True)
- select(*, stations=None, frequencies=None, components=None)[source]
Select survey axes by integer position.
- Parameters:
stations (sequence of int, optional) – Positions to retain. Omitted axes are retained completely. The requested order is preserved, but duplicates are rejected by the canonical unique-name/frequency validation.
frequencies (sequence of int, optional) – Positions to retain. Omitted axes are retained completely. The requested order is preserved, but duplicates are rejected by the canonical unique-name/frequency validation.
components (sequence of int, optional) – Positions to retain. Omitted axes are retained completely. The requested order is preserved, but duplicates are rejected by the canonical unique-name/frequency validation.
- Returns:
New immutable survey containing the requested subset.
- Return type:
- Raises:
ValueError – If an index collection is not one-dimensional or creates a non-monotonic frequency axis.
IndexError – If an index lies outside its axis.
Examples
>>> z = np.ones((2, 3, 2), dtype=complex) >>> s = SurveyData( ... z, [100, 10, 1], ["A", "B"], ["xy", "yx"], [[0, 0], [1, 0]] ... ) >>> subset = s.select(stations=[1], frequencies=[1, 2], components=[0]) >>> subset.station_names, subset.frequencies_hz.tolist() (('B',), [10.0, 1.0])
- select_names(*, stations=None, components=None, frequency_min_hz=None, frequency_max_hz=None)[source]
Select stations/components by name and frequencies by interval.
- Parameters:
stations (sequence of str, optional) – Exact names in the desired output order.
components (sequence of str, optional) – Exact names in the desired output order.
frequency_min_hz (float, optional) – Inclusive physical bounds. Their numerical order is independent of the stored frequency direction.
frequency_max_hz (float, optional) – Inclusive physical bounds. Their numerical order is independent of the stored frequency direction.
- Returns:
New validated subset.
- Return type:
- Raises:
KeyError – If a station or component name is unknown.
ValueError – If bounds are invalid or select no frequencies.
Examples
>>> z = np.ones((2, 3, 2), dtype=complex) >>> s = SurveyData( ... z, [100, 10, 1], ["A", "B"], ["xy", "yx"], [[0, 0], [1, 0]] ... ) >>> subset = s.select_names( ... stations=["B"], components=["yx"], frequency_min_hz=5 ... ) >>> subset.shape (1, 2, 1)
- assert_compatible(other, *, require_crs=True, require_convention=True)[source]
Assert that two surveys share model-facing axes and conventions.
- Parameters:
other (SurveyData) – Survey to compare.
require_crs (bool, default=True) – Require identical CRS identifiers.
require_convention (bool, default=True) – Require identical impedance conventions.
- Returns:
Successful return means the surveys can share a fitted normalizer or be concatenated along their station axes.
- Return type:
None
- Raises:
TypeError – If
otheris notSurveyData.ValueError – If frequency values/order, components/order, CRS, or convention differ under the requested policy.
Examples
>>> a = SurveyData( ... np.ones((1, 1, 1), complex), ... [1], ... ["A"], ... ["xy"], ... [[0, 0]], ... crs="EPSG:32630", ... ) >>> b = SurveyData( ... np.ones((1, 1, 1), complex), ... [1], ... ["B"], ... ["xy"], ... [[1, 0]], ... crs="EPSG:32630", ... ) >>> a.assert_compatible(b) is None True
- with_metadata(metadata, *, merge=True)[source]
Return a copy with validated provenance metadata.
- Parameters:
metadata (mapping) – Finite JSON-serializable values to add or use as replacement.
merge (bool, default=True) – Merge with existing metadata when true. New keys replace existing keys. When false, replace the complete metadata mapping.
- Returns:
New survey sharing no mutable numerical or metadata state.
- Return type:
Examples
>>> s = SurveyData( ... np.ones((1, 1, 1), complex), [1], ["S"], ["xy"], [[0, 0]] ... ) >>> tagged = s.with_metadata({"line": "L18", "rotation_deg": 0.0}) >>> tagged.metadata["line"] 'L18'
- summary()[source]
Return a compact JSON-serializable survey summary.
- Returns:
Axis sizes, bounds, coverage, CRS, component names, convention, and optional-data flags. Raw observations are not included.
- Return type:
Examples
>>> s = SurveyData( ... np.ones((1, 2, 1), complex), [10, 1], ["S"], ["xy"], [[0, 0]] ... ) >>> s.summary()["frequency_range_hz"] [1.0, 10.0]
- to_npz(path)[source]
Write a lossless, pickle-free compressed archive.
- Parameters:
path (str or pathlib.Path) – Destination
.npzpath. NumPy appends.npzwhen the supplied filename has no such suffix.- Returns:
Requested destination path.
- Return type:
Notes
The archive contains only numerical arrays and JSON/Unicode scalars.
from_npz()therefore loads it withallow_pickle=False.Examples
>>> from tempfile import TemporaryDirectory >>> s = SurveyData( ... np.ones((1, 1, 1), complex), [1], ["S"], ["xy"], [[0, 0]] ... ) >>> with TemporaryDirectory() as directory: ... path = s.to_npz(Path(directory) / "survey.npz") ... restored = SurveyData.from_npz(path) >>> restored.station_names ('S',)
- classmethod from_npz(path)[source]
Load and validate a survey archive without enabling pickle.
- Parameters:
path (str or pathlib.Path) – Archive previously written by
to_npz().- Returns:
Newly validated immutable survey.
- Return type:
- Raises:
ValueError – If the schema is unsupported or restored values violate the data contract.
OSError – If the path cannot be read as a NumPy archive.
Examples
>>> from tempfile import TemporaryDirectory >>> original = SurveyData( ... np.ones((1, 1, 1), complex), [1], ["S"], ["xy"], [[0, 0]] ... ) >>> with TemporaryDirectory() as directory: ... path = original.to_npz(Path(directory) / "survey.npz") ... loaded = SurveyData.from_npz(path) >>> np.array_equal(loaded.impedance, original.impedance) True
Notes
Schema version 1 remains readable and receives the default
ImpedanceConvention. New writes use schema version 2.
- pycsamt.ai.data.contracts.merge_surveys(surveys, *, metadata=None)[source]
Concatenate compatible surveys along the station axis.
- Parameters:
surveys (sequence of SurveyData) – Non-empty surveys with identical frequencies, component order, CRS, impedance convention, and optional-data availability. Station names must be globally unique.
metadata (mapping, optional) – Metadata for the merged object. By default a minimal provenance record containing
source_survey_countis used; input metadata are not combined implicitly because equal keys may have different meanings.
- Returns:
Canonical survey with concatenated station-axis arrays.
- Return type:
- Raises:
ValueError – If no surveys are supplied, surveys are incompatible, optional error or tipper availability differs, or station names overlap.
TypeError – If an item is not
SurveyData.
Examples
>>> a = SurveyData( ... np.ones((1, 2, 1), complex), [10, 1], ["A"], ["xy"], [[0, 0]] ... ) >>> b = SurveyData( ... np.ones((1, 2, 1), complex), [10, 1], ["B"], ["xy"], [[1, 0]] ... ) >>> merged = merge_surveys([a, b]) >>> merged.station_names, merged.shape (('A', 'B'), (2, 2, 1))
Notes
This function performs no frequency interpolation, coordinate projection, tensor rotation, or unit conversion. Those operations must be explicit and completed before merging.