2.25.3.5. pycsamt.ai.domain_gap#

Survey auditing, corruption simulation, survey-fit estimation, and feature distribution comparisons for synthetic-to-field domain-gap analysis.

Domain-gap and noise simulation for AI-assisted EM inversion (M3).

This package turns a clean SurveyData into realistic training data by injecting heteroscedastic noise, dropout, static shift, galvanic distortion, coordinate perturbation, and outliers (simulator); by fitting plausible parameter ranges from a real survey’s own QC diagnostics, AMT, CSAMT, MT, or otherwise (survey_fit); and by comparing simulated and field feature distributions quantitatively (report).

class pycsamt.ai.domain_gap.StationExclusion(station, reason)#

Bases: object

One station dropped before the canonical survey bridge.

Parameters:
  • station (str) – Station identifier as reported by the raw site collection.

  • reason (str) – Human-readable, non-empty exclusion reason.

Examples

>>> StationExclusion("18-099Z", "missing freq or z array").reason
'missing freq or z array'
station: str#
reason: str#
to_dict()#

Return a JSON-serializable representation.

Returns:

station and reason fields.

Return type:

dict

Examples

>>> StationExclusion("A", "missing freq or z array").to_dict()
{'station': 'A', 'reason': 'missing freq or z array'}
class pycsamt.ai.domain_gap.FrequencyGridReport(matched, reference_station, n_frequencies_by_station, mismatched_stations)#

Bases: object

Whether every included station shares one frequency grid.

Parameters:
  • matched (bool) – True only when every included station’s frequency grid equals the reference station’s, within tolerance.

  • reference_station (str or None) – Station whose grid every other station was compared against.

  • n_frequencies_by_station (mapping) – Station name to its own frequency count, for every included station, regardless of whether it matched.

  • mismatched_stations (tuple of str) – Included stations whose frequency grid differs from the reference. Empty when matched is True.

Examples

>>> report = FrequencyGridReport(True, "A", {"A": 10, "B": 10}, ())
>>> report.matched
True
matched: bool#
reference_station: str | None#
n_frequencies_by_station: Mapping[str, int]#
mismatched_stations: tuple[str, ...]#
to_dict()#

Return a JSON-serializable representation.

Returns:

All fields as plain JSON-compatible values.

Return type:

dict

Examples

>>> FrequencyGridReport(True, "A", {"A": 1}, ()).to_dict()["matched"]
True
class pycsamt.ai.domain_gap.DimensionalitySummary(n_samples, frac_1d, frac_2d, frac_3d, strike_consensus_deg, strike_consensus_iqr_deg, stations_recommending_3d_review=())#

Bases: object

Survey-wide aggregate of pre2d_inversion_assessment().

Parameters:
  • n_samples (int) – Total station-period samples the per-station table was built from.

  • frac_1d (float) – Sample-weighted fraction classified 1-D, 2-D, and 3-D. They sum to one when n_samples is positive.

  • frac_2d (float) – Sample-weighted fraction classified 1-D, 2-D, and 3-D. They sum to one when n_samples is positive.

  • frac_3d (float) – Sample-weighted fraction classified 1-D, 2-D, and 3-D. They sum to one when n_samples is positive.

  • strike_consensus_deg (float or None) – Median across stations of each station’s consensus strike angle and its interquartile spread; None when no station produced a finite value.

  • strike_consensus_iqr_deg (float or None) – Median across stations of each station’s consensus strike angle and its interquartile spread; None when no station produced a finite value.

  • stations_recommending_3d_review (tuple of str) – Stations pre2d_inversion_assessment() flagged "review_3d_effects_before_2d".

Examples

>>> summary = DimensionalitySummary(10, 0.7, 0.2, 0.1, 12.0, 5.0, ())
>>> round(summary.frac_1d + summary.frac_2d + summary.frac_3d, 6)
1.0
n_samples: int#
frac_1d: float#
frac_2d: float#
frac_3d: float#
strike_consensus_deg: float | None#
strike_consensus_iqr_deg: float | None#
stations_recommending_3d_review: tuple[str, ...] = ()#
to_dict()#

Return a JSON-serializable representation.

Returns:

All fields as plain JSON-compatible values.

Return type:

dict

Examples

>>> DimensionalitySummary(1, 1.0, 0.0, 0.0, None, None).to_dict()[
...     "n_samples"
... ]
1
class pycsamt.ai.domain_gap.SurveyAuditReport(n_stations_input, excluded_stations, frequency_grid, coverage, frequency_range_hz, error_ratio_p05, error_ratio_p50, error_ratio_p95, station_spacing_m, elevation_coverage, crs_declared, dimensionality, static_shift_log10_sigma, distortion_gain_log10_sigma, distortion_twist_deg_sigma, distortion_shear_sigma, distortion_anisotropy_sigma, generated_utc, metadata=<factory>)#

Bases: object

Complete M1 accounting of one survey’s data quality.

Parameters:
  • n_stations_input (int) – Stations found by ensure_sites() before any exclusion.

  • excluded_stations (tuple of StationExclusion) – Every station dropped before the canonical bridge, with a reason.

  • frequency_grid (FrequencyGridReport) – Whether every included station shares one frequency grid.

  • coverage (SurveyCoverage or None) – Impedance coverage from coverage(), or None when frequency_grid did not match (the canonical bridge cannot run without a shared grid).

  • frequency_range_hz ((float, float) or None) – Minimum and maximum frequency, when available.

  • error_ratio_p05 (float or None) – 5th/50th/95th percentile of declared impedance_error / |Z| over valid observations, when available.

  • error_ratio_p50 (float or None) – 5th/50th/95th percentile of declared impedance_error / |Z| over valid observations, when available.

  • error_ratio_p95 (float or None) – 5th/50th/95th percentile of declared impedance_error / |Z| over valid observations, when available.

  • station_spacing_m (mapping or None) – min, median, and max nearest-neighbour station spacing (independent of station order), plus aperture, the largest inter-station distance.

  • elevation_coverage (float) – Fraction of included stations with a finite elevation.

  • crs_declared (bool) – Whether a coordinate reference system was supplied explicitly. Station coordinates for MT/AMT sites are ordinarily projected locally from latitude/longitude, which by itself declares no formal CRS.

  • dimensionality (DimensionalitySummary) – Aggregate dimensionality and strike indicators.

  • static_shift_log10_sigma (float) – Empirical spreads from fit_distortion_priors_from_sites().

  • distortion_gain_log10_sigma (float) – Empirical spreads from fit_distortion_priors_from_sites().

  • distortion_twist_deg_sigma (float) – Empirical spreads from fit_distortion_priors_from_sites().

  • distortion_shear_sigma (float) – Empirical spreads from fit_distortion_priors_from_sites().

  • distortion_anisotropy_sigma (float) – Empirical spreads from fit_distortion_priors_from_sites().

  • generated_utc (str) – Timezone-aware ISO-8601 timestamp of when the audit ran.

  • metadata (Mapping[str, Any])

Examples

Reports are normally produced by audit_survey(), not built directly:

>>> report = SurveyAuditReport(
...     n_stations_input=1,
...     excluded_stations=(),
...     frequency_grid=FrequencyGridReport(True, "A", {"A": 1}, ()),
...     coverage=None,
...     frequency_range_hz=None,
...     error_ratio_p05=None,
...     error_ratio_p50=None,
...     error_ratio_p95=None,
...     station_spacing_m=None,
...     elevation_coverage=0.0,
...     crs_declared=False,
...     dimensionality=DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     static_shift_log10_sigma=0.0,
...     distortion_gain_log10_sigma=0.0,
...     distortion_twist_deg_sigma=0.0,
...     distortion_shear_sigma=0.0,
...     distortion_anisotropy_sigma=0.0,
...     generated_utc="2026-01-01T00:00:00Z",
... )
>>> report.n_stations_included
1
n_stations_input: int#
excluded_stations: tuple[StationExclusion, ...]#
frequency_grid: FrequencyGridReport#
coverage: SurveyCoverage | None#
frequency_range_hz: tuple[float, float] | None#
error_ratio_p05: float | None#
error_ratio_p50: float | None#
error_ratio_p95: float | None#
station_spacing_m: Mapping[str, float] | None#
elevation_coverage: float#
crs_declared: bool#
dimensionality: DimensionalitySummary#
static_shift_log10_sigma: float#
distortion_gain_log10_sigma: float#
distortion_twist_deg_sigma: float#
distortion_shear_sigma: float#
distortion_anisotropy_sigma: float#
generated_utc: str#
metadata: Mapping[str, Any]#
property n_stations_included: int#

Return the number of stations that reached the canonical bridge.

Returns:

n_stations_input minus the number of exclusions.

Return type:

int

Examples

Constructed directly for illustration; see audit_survey() for the normal way to obtain a report.

>>> report = SurveyAuditReport(
...     2,
...     (StationExclusion("B", "missing freq or z array"),),
...     FrequencyGridReport(True, "A", {"A": 1}, ()),
...     None,
...     None,
...     None,
...     None,
...     None,
...     None,
...     0.0,
...     False,
...     DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     "2026-01-01T00:00:00Z",
... )
>>> report.n_stations_included
1
to_dict()#

Return a complete JSON-serializable representation.

Returns:

Every field, with nested records converted to plain dicts.

Return type:

dict

Examples

>>> report = SurveyAuditReport(
...     1,
...     (),
...     FrequencyGridReport(True, "A", {"A": 1}, ()),
...     None,
...     None,
...     None,
...     None,
...     None,
...     None,
...     0.0,
...     False,
...     DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     "2026-01-01T00:00:00Z",
... )
>>> report.to_dict()["schema_version"]
1
write_json(path, *, overwrite=True)#

Write a deterministic, human-readable audit report file.

Parameters:
  • path (str or pathlib.Path) – Destination JSON file.

  • overwrite (bool, default=True) – Permit replacement of an existing file.

Returns:

Destination path.

Return type:

pathlib.Path

Raises:

FileExistsError – If the destination exists and overwrite is false.

Examples

>>> from tempfile import TemporaryDirectory
>>> report = SurveyAuditReport(
...     1,
...     (),
...     FrequencyGridReport(True, "A", {"A": 1}, ()),
...     None,
...     None,
...     None,
...     None,
...     None,
...     None,
...     0.0,
...     False,
...     DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     "2026-01-01T00:00:00Z",
... )
>>> with TemporaryDirectory() as directory:
...     path = report.write_json(Path(directory) / "audit.json")
...     loaded = SurveyAuditReport.read_json(path)
>>> loaded.n_stations_input
1
classmethod from_dict(data)#

Restore a report from its JSON representation.

Parameters:

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

Returns:

Validated immutable report.

Return type:

SurveyAuditReport

Raises:

ValueError – If the schema version is unsupported.

Examples

>>> report = SurveyAuditReport(
...     1,
...     (),
...     FrequencyGridReport(True, "A", {"A": 1}, ()),
...     None,
...     None,
...     None,
...     None,
...     None,
...     None,
...     0.0,
...     False,
...     DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     "2026-01-01T00:00:00Z",
... )
>>> SurveyAuditReport.from_dict(report.to_dict()) == report
True
classmethod read_json(path)#

Read and validate a UTF-8 JSON audit report.

Parameters:

path (str or pathlib.Path) – Existing report file written by write_json().

Returns:

Validated immutable report.

Return type:

SurveyAuditReport

Examples

See write_json() for a complete round trip.

summary()#

Return a compact, human-readable multi-line report.

Returns:

Plain-text accounting of inclusion, coverage, geometry, dimensionality, and distortion indicators.

Return type:

str

Examples

>>> report = SurveyAuditReport(
...     1,
...     (),
...     FrequencyGridReport(True, "A", {"A": 1}, ()),
...     None,
...     None,
...     None,
...     None,
...     None,
...     None,
...     0.0,
...     False,
...     DimensionalitySummary(0, 0.0, 0.0, 0.0, None, None),
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     0.0,
...     "2026-01-01T00:00:00Z",
... )
>>> print(report.summary())
Survey audit (generated 2026-01-01T00:00:00Z)
  Stations: 1 input, 1 included, 0 excluded
  Frequency grid: matched
  CRS declared: False
  Elevation coverage: 0.0%
  Dimensionality: n=0, 1D=0.0%, 2D=0.0%, 3D=0.0%
  Static shift log10 sigma: 0.0000
  Distortion sigma: gain(log10)=0.0000, twist_deg=0.00, shear=0.0000, anisotropy=0.0000
pycsamt.ai.domain_gap.audit_survey(sites, *, recursive=True, on_dup='replace', verbose=0, freq_rtol=1e-06, band=None, skew_th=3.0, ellipt_th=0.2, station_spacing_fallback=500.0, metadata=None)#

Audit a raw survey and account for every included/excluded station.

Unlike survey_data_from_sites(), which deliberately raises on a mismatched frequency grid so training can never proceed silently on inconsistent data, this function never raises for that reason: it is meant to be run before deciding whether a survey is fit for that stricter bridge, and reports a mismatch as a finding rather than an exception.

Parameters:
  • sites (Any) – Anything accepted by pycsamt.emtools._core.ensure_sites().

  • recursive (bool) – Forwarded to ensure_sites and the underlying diagnostics.

  • on_dup (str) – Forwarded to ensure_sites and the underlying diagnostics.

  • verbose (int) – Forwarded to ensure_sites and the underlying diagnostics.

  • freq_rtol (float, default=1e-6) – Relative tolerance used when comparing each station’s frequency grid against the reference station’s.

  • band ((float, float), optional) – Period band in seconds forwarded to pre2d_inversion_assessment().

  • skew_th (float, optional) – Phase-tensor skew and ellipticity thresholds forwarded to the same dimensionality assessment.

  • ellipt_th (float, optional) – Phase-tensor skew and ellipticity thresholds forwarded to the same dimensionality assessment.

  • station_spacing_fallback (float, default=500.0) – Uniform-grid spacing in metres used only when no station reports usable latitude/longitude.

  • metadata (mapping, optional) – Extra provenance recorded on the returned report.

Returns:

Complete accounting of inclusion, coverage, geometry, dimensionality, and distortion indicators.

Return type:

SurveyAuditReport

Raises:

ValueError – If no station in sites has usable impedance data at all.

Examples

>>> report = audit_survey(
...     "data/AMT/WILLY_data/L18PLT", recursive=True, verbose=0
... )
>>> report.frequency_grid.matched
True
class pycsamt.ai.domain_gap.EmpiricalCorruptionResult(survey, seed, field_station_indices, static_log10_resistivity_factor, relative_error_fraction, noise_realization, missing_mask, measurement_reliability, dimensionality_reliability, observation_reliability)#

Bases: object

One corrupted survey and every sampled latent array.

Parameters:
  • survey (SurveyData) – Corrupted survey with declared empirical impedance errors.

  • seed (int) – Parent random seed.

  • field_station_indices (ndarray of int) – Empirical field station selected for each synthetic station.

  • static_log10_resistivity_factor (ndarray) – Sampled log10(rho_observed / rho_smooth).

  • relative_error_fraction (ndarray) – Sampled impedance-error fractions by component.

  • noise_realization (ndarray of complex) – Additive complex noise in impedance units.

  • missing_mask (ndarray of bool) – True where an observation was removed.

  • measurement_reliability (ndarray) – Separate and combined reliability factors.

  • dimensionality_reliability (ndarray) – Separate and combined reliability factors.

  • observation_reliability (ndarray) – Separate and combined reliability factors.

survey: SurveyData#
seed: int#
field_station_indices: ndarray#
static_log10_resistivity_factor: ndarray#
relative_error_fraction: ndarray#
noise_realization: ndarray#
missing_mask: ndarray#
measurement_reliability: ndarray#
dimensionality_reliability: ndarray#
observation_reliability: ndarray#
property record_hash: str#

Return a deterministic hash of seed and sampled summaries.

Returns:

SHA-256 digest of JSON-compatible provenance.

Return type:

str

to_dict()#

Return compact JSON-compatible corruption provenance.

Returns:

Seed, selected field stations, and sampled ranges.

Return type:

dict

pycsamt.ai.domain_gap.apply_empirical_corruption(survey, *, field_frequencies_hz, static_log10_resistivity_profiles, measurement_reliability_profiles, dimensionality_reliability_profiles, observation_reliability_profiles, relative_error_quantiles, seed, missing_rate_by_component=None)#

Apply jointly sampled field profiles and empirical error quantiles.

Parameters:
  • survey (SurveyData) – Clean synthetic survey.

  • field_frequencies_hz (array-like) – Positive unique frequencies of the empirical field profiles.

  • static_log10_resistivity_profiles (array-like) – Field log10(rho_observed / rho_smooth) profiles shaped (n_field_station, n_field_frequency).

  • measurement_reliability_profiles (array-like) – Aligned empirical reliability profiles in [0, 1].

  • dimensionality_reliability_profiles (array-like) – Aligned empirical reliability profiles in [0, 1].

  • observation_reliability_profiles (array-like) – Aligned empirical reliability profiles in [0, 1].

  • relative_error_quantiles (mapping) – Component names mapped to {"levels": ..., "values": ...} monotone empirical quantile curves.

  • seed (int) – Parent random seed.

  • missing_rate_by_component (mapping or None, optional) – Empirical independent missing probabilities. Missing observations are set to complex NaN and marked invalid.

Returns:

Corrupted survey and complete sampled provenance.

Return type:

EmpiricalCorruptionResult

Notes

Static shift is supplied in apparent-resistivity space and therefore applied to impedance as 10**(0.5 * log10_rho_factor).

Examples

>>> clean = SurveyData(
...     np.ones((2, 2, 1), complex),
...     [10.0, 1.0],
...     ["A", "B"],
...     ["zxy"],
...     [[0, 0], [1, 0]],
... )
>>> result = apply_empirical_corruption(
...     clean,
...     field_frequencies_hz=[10.0, 1.0],
...     static_log10_resistivity_profiles=[[0.0, 0.0]],
...     measurement_reliability_profiles=[[0.8, 0.7]],
...     dimensionality_reliability_profiles=[[1.0, 0.5]],
...     observation_reliability_profiles=[[0.8, 0.35]],
...     relative_error_quantiles={
...         "zxy": {"levels": [0, 1], "values": [0.01, 0.05]}
...     },
...     seed=0,
... )
>>> result.survey.shape
(2, 2, 1)
class pycsamt.ai.domain_gap.CorruptionConfig(noise_level_range=(0.0, 0.0), error_floor_fraction=0.0, static_shift_log10_sigma=0.0, distortion_gain_log10_sigma=0.0, distortion_twist_deg_sigma=0.0, distortion_shear_sigma=0.0, distortion_anisotropy_sigma=0.0, station_dropout_rate=0.0, frequency_dropout_rate=0.0, random_dropout_rate=0.0, outlier_rate=0.0, outlier_log10_shift_range=(0.5, 1.5), coordinate_sigma_m=0.0, elevation_sigma_m=0.0, noise_sampling='uniform')#

Bases: object

Parameter ranges for one corruption pass over a SurveyData.

All defaults are zero/no-op so CorruptionConfig() is the clean synthetic control set required by the M3 gate.

Parameters:
  • noise_level_range ((float, float), default=(0.0, 0.0)) – Bounds on the relative heteroscedastic noise standard deviation sampled independently per station/frequency observation.

  • error_floor_fraction (float, default=0.0) – Minimum declared impedance_error as a fraction of |Z|.

  • static_shift_log10_sigma (float, default=0.0) – Std. dev. of the log10 per-station static-shift factor applied identically across all frequencies.

  • distortion_gain_log10_sigma (float, default=0.0) – Std. dev. of the per-station Groom-Bailey-style gain, twist, shear, and anisotropy parameters of the injected galvanic distortion.

  • distortion_twist_deg_sigma (float, default=0.0) – Std. dev. of the per-station Groom-Bailey-style gain, twist, shear, and anisotropy parameters of the injected galvanic distortion.

  • distortion_shear_sigma (float, default=0.0) – Std. dev. of the per-station Groom-Bailey-style gain, twist, shear, and anisotropy parameters of the injected galvanic distortion.

  • distortion_anisotropy_sigma (float, default=0.0) – Std. dev. of the per-station Groom-Bailey-style gain, twist, shear, and anisotropy parameters of the injected galvanic distortion.

  • station_dropout_rate (float, default=0.0) – Probability that an entire station, an entire frequency (across all stations), or an individual observation is marked missing.

  • frequency_dropout_rate (float, default=0.0) – Probability that an entire station, an entire frequency (across all stations), or an individual observation is marked missing.

  • random_dropout_rate (float, default=0.0) – Probability that an entire station, an entire frequency (across all stations), or an individual observation is marked missing.

  • outlier_rate (float, default=0.0) – Fraction of remaining valid observations perturbed by a large, undetected multiplicative shift.

  • outlier_log10_shift_range ((float, float), default=(0.5, 1.5)) – Bounds on the magnitude (in log10 decades) of injected outliers; the sign is randomized.

  • coordinate_sigma_m (float, default=0.0) – Std. dev. of Gaussian perturbation applied to station horizontal coordinates and elevation, respectively.

  • elevation_sigma_m (float, default=0.0) – Std. dev. of Gaussian perturbation applied to station horizontal coordinates and elevation, respectively.

  • noise_sampling ({"uniform", "loguniform", "lognormal"}, default="uniform") – How the relative noise level is drawn from noise_level_range; see add_heteroscedastic_noise(). With "lognormal" the range is read as the 25th and 75th percentiles.

Examples

>>> config = CorruptionConfig(noise_level_range=(0.01, 0.05))
>>> config.config_hash() == CorruptionConfig(
...     noise_level_range=(0.01, 0.05)
... ).config_hash()
True
noise_level_range: tuple[float, float] = (0.0, 0.0)#
error_floor_fraction: float = 0.0#
static_shift_log10_sigma: float = 0.0#
distortion_gain_log10_sigma: float = 0.0#
distortion_twist_deg_sigma: float = 0.0#
distortion_shear_sigma: float = 0.0#
distortion_anisotropy_sigma: float = 0.0#
station_dropout_rate: float = 0.0#
frequency_dropout_rate: float = 0.0#
random_dropout_rate: float = 0.0#
outlier_rate: float = 0.0#
outlier_log10_shift_range: tuple[float, float] = (0.5, 1.5)#
coordinate_sigma_m: float = 0.0#
elevation_sigma_m: float = 0.0#
noise_sampling: str = 'uniform'#
to_dict()#

Return a JSON-serializable, order-stable representation.

Returns:

Field values with a schema discriminator.

Return type:

dict

Examples

>>> CorruptionConfig().to_dict()["schema_version"]
1
classmethod from_dict(data)#

Restore and validate a serialized configuration.

Parameters:

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

Returns:

Validated immutable configuration.

Return type:

CorruptionConfig

Examples

>>> state = CorruptionConfig(error_floor_fraction=0.02).to_dict()
>>> CorruptionConfig.from_dict(state).error_floor_fraction
0.02
config_hash()#

Return the SHA-256 digest of this configuration’s canonical JSON.

Returns:

Lowercase 64-character hexadecimal digest.

Return type:

str

Examples

>>> len(CorruptionConfig().config_hash())
64
class pycsamt.ai.domain_gap.CorruptionRecord(config, seed, sampled=<factory>, severity=None)#

Bases: object

Provenance of one applied apply_corruption_suite() call.

Parameters:
  • config (CorruptionConfig) – Configuration the sampled parameters were drawn from.

  • seed (int) – Parent seed used to spawn every corruption step’s generator.

  • severity (str or None) – Name of the severity preset used, when applicable.

  • sampled (mapping) – Concrete, JSON-serializable per-step summary statistics (e.g. the number of dropped stations, or the mean sampled distortion gain).

config: CorruptionConfig#
seed: int#
sampled: Mapping[str, Any]#
severity: str | None = None#
to_dict()#

Return a JSON-serializable provenance record.

Returns:

Schema-versioned config, seed, severity, and sampled summary.

Return type:

dict

Examples

>>> record = CorruptionRecord(CorruptionConfig(), seed=0)
>>> record.to_dict()["schema_version"]
1
pycsamt.ai.domain_gap.add_heteroscedastic_noise(survey, *, level_range=(0.02, 0.05), rng, sampling='uniform')#

Add complex, per-observation heteroscedastic Gaussian noise.

Parameters:
  • survey (SurveyData) – Clean or already-corrupted survey.

  • level_range ((float, float), default=(0.02, 0.05)) – Bounds on the relative noise standard deviation sampled independently for every (station, frequency) pair and shared across components at that pair, mimicking correlated instrument noise.

  • rng (numpy.random.Generator) – Source of randomness; callers control reproducibility.

  • sampling ({"uniform", "loguniform", "lognormal"}, default="uniform") – Distribution of the relative noise level. "uniform" draws uniformly between the bounds; "loguniform" draws uniformly in log10 between them; "lognormal" reads level_range as the 25th and 75th percentiles of a log-normal distribution (median sqrt(lo * hi), log10 standard deviation log10(hi / lo) / 1.349) truncated at its 99.9th percentile. Declared field errors are usually spread over orders of magnitude, which the log-scale options reproduce.

Returns:

New survey with perturbed impedance and an impedance_error that combines any pre-existing error with the injected noise in quadrature.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((1, 2, 1), 100 + 0j)
>>> survey = SurveyData(z, [10.0, 1.0], ["S"], ["xy"], [[0.0, 0.0]])
>>> noisy = add_heteroscedastic_noise(
...     survey, level_range=(0.05, 0.05), rng=np.random.default_rng(0)
... )
>>> noisy.impedance_error is not None
True
pycsamt.ai.domain_gap.apply_corruption_suite(survey, config=None, *, severity=None, seed)#

Apply the full, ordered M3 corruption pipeline from a single seed.

Steps run in this fixed order: static shift, galvanic distortion, heteroscedastic noise, error floor, dropout, outliers, coordinate perturbation. Systematic distortions are applied to the clean signal before random noise and missingness, matching how these effects compose physically.

Parameters:
  • survey (SurveyData) – Clean survey to corrupt.

  • config (CorruptionConfig, optional) – Explicit configuration. Mutually exclusive with severity.

  • severity (str, optional) – Name of an entry in SEVERITY_PRESETS to use as config.

  • seed (int) – Parent seed. Each step draws from an independently spawned child generator so adding a new step does not change earlier steps’ draws.

Returns:

  • survey (SurveyData) – Corrupted survey.

  • record (CorruptionRecord) – Provenance of the applied configuration and sampled parameters.

Raises:

ValueError – If both or neither of config/severity are given, or severity is unknown.

Return type:

tuple[SurveyData, CorruptionRecord]

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((3, 5, 2), 100 + 50j)
>>> survey = SurveyData(
...     z,
...     np.linspace(100, 1, 5),
...     ["A", "B", "C"],
...     ["xy", "yx"],
...     np.zeros((3, 2)),
... )
>>> corrupted, record = apply_corruption_suite(
...     survey, severity="in_distribution", seed=0
... )
>>> record.severity
'in_distribution'
pycsamt.ai.domain_gap.apply_dropout(survey, *, station_rate=0.0, frequency_rate=0.0, random_rate=0.0, rng, return_info=False)#

Mark stations, frequencies, or individual observations as missing.

Dropped observations are invalidated by setting the impedance to NaN; SurveyData construction then recomputes valid from finiteness, so masks stay authoritative automatically.

Parameters:
  • survey (SurveyData) – Survey to thin out.

  • station_rate (float, default=0.0) – Independent probabilities that a whole station (all frequencies and components), a whole frequency (all stations and components), or an individual observation is dropped. Effects are combined (a station or frequency dropout wins over a random one at the same cell).

  • frequency_rate (float, default=0.0) – Independent probabilities that a whole station (all frequencies and components), a whole frequency (all stations and components), or an individual observation is dropped. Effects are combined (a station or frequency dropout wins over a random one at the same cell).

  • random_rate (float, default=0.0) – Independent probabilities that a whole station (all frequencies and components), a whole frequency (all stations and components), or an individual observation is dropped. Effects are combined (a station or frequency dropout wins over a random one at the same cell).

  • rng (numpy.random.Generator) – Source of randomness.

  • return_info (bool)

Returns:

New survey with additional invalid observations.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((4, 4, 1), dtype=complex)
>>> survey = SurveyData(
...     z,
...     [4.0, 3.0, 2.0, 1.0],
...     ["A", "B", "C", "D"],
...     ["xy"],
...     np.zeros((4, 2)),
... )
>>> thinned = apply_dropout(
...     survey, station_rate=1.0, rng=np.random.default_rng(0)
... )
>>> thinned.n_valid
0
pycsamt.ai.domain_gap.apply_error_floor(survey, *, floor_fraction)#

Clamp the declared error to a minimum fraction of |Z|.

Parameters:
  • survey (SurveyData) – Survey whose error floor should be enforced.

  • floor_fraction (float) – Minimum impedance_error as a fraction of |Z|. Zero is a no-op.

Returns:

New survey with an error array at least as large as floor_fraction * |Z| on valid observations.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((1, 1, 1), 100 + 0j)
>>> survey = SurveyData(
...     z,
...     [1.0],
...     ["S"],
...     ["xy"],
...     [[0.0, 0.0]],
...     impedance_error=np.ones((1, 1, 1)),
... )
>>> floored = apply_error_floor(survey, floor_fraction=0.5)
>>> floored.impedance_error[0, 0, 0]
50.0
pycsamt.ai.domain_gap.apply_galvanic_distortion(survey, *, gain_log10_sigma=0.0, twist_deg_sigma=0.0, shear_sigma=0.0, anisotropy_sigma=0.0, rng, return_info=False)#

Inject a per-station real Groom & Bailey-style distortion matrix.

Parameters:
  • survey (SurveyData) – Survey to distort. Must expose at least one impedance component; any of xx, xy, yx, yy absent from SurveyData.components is treated as zero for the purpose of building the dense 2x2 impedance used internally, which is an approximation for surveys that only store off-diagonal components.

  • gain_log10_sigma (float) – Std. dev. of the per-station gain (log10), twist (degrees), shear, and anisotropy parameters. Zero for every parameter is a no-op.

  • twist_deg_sigma (float) – Std. dev. of the per-station gain (log10), twist (degrees), shear, and anisotropy parameters. Zero for every parameter is a no-op.

  • shear_sigma (float) – Std. dev. of the per-station gain (log10), twist (degrees), shear, and anisotropy parameters. Zero for every parameter is a no-op.

  • anisotropy_sigma (float) – Std. dev. of the per-station gain (log10), twist (degrees), shear, and anisotropy parameters. Zero for every parameter is a no-op.

  • rng (numpy.random.Generator) – Source of randomness.

  • return_info (bool)

Returns:

New survey with distorted impedance; declared error, if any, is scaled by the sampled gain as a first-order approximation.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((1, 1, 2), dtype=complex) * (1 + 1j)
>>> survey = SurveyData(z, [1.0], ["S"], ["xy", "yx"], [[0, 0]])
>>> distorted = apply_galvanic_distortion(
...     survey, twist_deg_sigma=10.0, rng=np.random.default_rng(0)
... )
>>> distorted.shape == survey.shape
True
pycsamt.ai.domain_gap.apply_static_shift(survey, *, log10_sigma, rng, return_info=False)#

Multiply every present component by a per-station real factor.

Static shift is modelled as a frequency-independent real scalar c_s = 10 ** N(0, log10_sigma) per station, applied identically to every impedance component and to the declared error, consistent with the linear scaling of a real multiplicative distortion.

Parameters:
Returns:

New survey with static shift applied.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((2, 1, 1), 100 + 0j)
>>> survey = SurveyData(z, [1.0], ["A", "B"], ["xy"], [[0, 0], [1, 0]])
>>> shifted = apply_static_shift(
...     survey, log10_sigma=0.1, rng=np.random.default_rng(0)
... )
>>> shifted.shape == survey.shape
True
pycsamt.ai.domain_gap.apply_structured_dropout(survey, *, dropout_probability, rng, return_info=False)#

Mark observations missing from an externally supplied probability field.

Unlike apply_dropout()’s three independent scalar rates (which are mathematically forced to be equal whenever they are each derived from the same overall coverage deficit, and which compound multiplicatively when applied as independent channels), this drops each (station, frequency, component) cell according to its own fitted Bernoulli probability, allowing missingness that is concentrated in particular stations or frequency bands rather than spread uniformly. Typical usage passes pycsamt.ai.domain_gap.survey_fit.StructuredMissingness.dropout_probability() fit from a real survey’s own coverage mask.

Parameters:
  • survey (SurveyData) – Survey to thin out.

  • dropout_probability (ndarray, shape (n_station, n_frequency, n_component)) – Per-cell probability that the observation is dropped. Must match survey.shape exactly, since the probability field is tied to specific station/frequency/component identities rather than being shape-agnostic like apply_dropout()’s scalar rates.

  • rng (numpy.random.Generator) – Source of randomness.

  • return_info (bool)

Returns:

New survey with additional invalid observations.

Return type:

SurveyData

Raises:

ValueError – If dropout_probability does not have survey.shape.

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((2, 3, 1), dtype=complex)
>>> survey = SurveyData(
...     z, [3.0, 2.0, 1.0], ["A", "B"], ["xy"], np.zeros((2, 2))
... )
>>> pi = np.full((2, 3, 1), 1.0)
>>> thinned = apply_structured_dropout(
...     survey, dropout_probability=pi, rng=np.random.default_rng(0)
... )
>>> thinned.n_valid
0
pycsamt.ai.domain_gap.inject_outliers(survey, *, rate=0.0, log10_shift_range=(0.5, 1.5), rng, return_info=False)#

Perturb a random fraction of valid observations by a large factor.

Outliers remain marked valid and keep their existing declared error, simulating a bad reading that quality control failed to flag — the case a robust inverter must tolerate.

Parameters:
  • survey (SurveyData) – Survey to perturb.

  • rate (float, default=0.0) – Fraction of currently valid observations perturbed. Zero is a no-op.

  • log10_shift_range ((float, float), default=(0.5, 1.5)) – Bounds on the outlier magnitude in log10 decades; the sign is randomized per outlier.

  • rng (numpy.random.Generator) – Source of randomness.

  • return_info (bool)

Returns:

New survey with a subset of valid impedance values shifted by 10 ** (+/- shift).

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((1, 10, 1), 100 + 0j)
>>> survey = SurveyData(
...     z, np.arange(10.0, 0.0, -1.0), ["S"], ["xy"], [[0, 0]]
... )
>>> corrupted = inject_outliers(
...     survey, rate=0.5, rng=np.random.default_rng(0)
... )
>>> corrupted.shape == survey.shape
True
pycsamt.ai.domain_gap.perturb_coordinates(survey, *, coordinate_sigma_m=0.0, elevation_sigma_m=0.0, rng)#

Add Gaussian noise to station coordinates and elevation.

Parameters:
  • survey (SurveyData) – Survey whose station geometry should be perturbed.

  • coordinate_sigma_m (float, default=0.0) – Std. dev. of Gaussian noise added to the horizontal (x, y) coordinates and to elevation, respectively. Elevation entries that are NaN (unknown) stay NaN.

  • elevation_sigma_m (float, default=0.0) – Std. dev. of Gaussian noise added to the horizontal (x, y) coordinates and to elevation, respectively. Elevation entries that are NaN (unknown) stay NaN.

  • rng (numpy.random.Generator) – Source of randomness.

Returns:

New survey with perturbed SurveyData.coordinates_m.

Return type:

SurveyData

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((1, 1, 1), dtype=complex)
>>> survey = SurveyData(z, [1.0], ["S"], ["xy"], [[0.0, 0.0, 100.0]])
>>> moved = perturb_coordinates(
...     survey, coordinate_sigma_m=5.0, rng=np.random.default_rng(0)
... )
>>> moved.coordinates_m.shape
(1, 3)
class pycsamt.ai.domain_gap.StructuredMissingness(alpha, station_effect, frequency_effect, component_effect, station_names, frequencies_hz, components, l2=1.0)#

Bases: object

Fitted station+frequency+component logistic missingness model.

logit(pi_sjc) = alpha + a_s + b_j + q_c, with the station, frequency, and component effects a, b, q each constrained to sum to zero for identifiability (an effect relative to its own group’s mean, not to an arbitrary reference level). This replaces the three independent scalar dropout rates of fit_corruption_config()/apply_dropout() – which are mathematically forced to be numerically identical whenever each is the mean of the same overall coverage deficit taken along a different axis – with an explicit model of where missingness concentrates. Fit by fit_structured_missingness().

Parameters:
  • alpha (float) – Intercept: logit of the fitted overall mean dropout probability.

  • station_effect (tuple of float) – Per-station, per-frequency, and per-component effects, each summing to zero by construction.

  • frequency_effect (tuple of float) – Per-station, per-frequency, and per-component effects, each summing to zero by construction.

  • component_effect (tuple of float) – Per-station, per-frequency, and per-component effects, each summing to zero by construction.

  • station_names (tuple) – Identities the effects are indexed by, kept for provenance. The fitted probability field is tied to this specific grid; it is not meant to be reused against a differently shaped survey.

  • frequencies_hz (tuple) – Identities the effects are indexed by, kept for provenance. The fitted probability field is tied to this specific grid; it is not meant to be reused against a differently shaped survey.

  • components (tuple) – Identities the effects are indexed by, kept for provenance. The fitted probability field is tied to this specific grid; it is not meant to be reused against a differently shaped survey.

  • l2 (float, default=1.0) – L2 penalty weight used during fitting (not applied to alpha).

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((3, 4, 1), dtype=complex)
>>> z[0, :2] = np.nan
>>> survey = SurveyData(
...     z, [4.0, 3.0, 2.0, 1.0], ["A", "B", "C"], ["xy"],
...     np.zeros((3, 2)),
... )
>>> model = fit_structured_missingness(survey)
>>> model.dropout_probability().shape
(3, 4, 1)
alpha: float#
station_effect: tuple[float, ...]#
frequency_effect: tuple[float, ...]#
component_effect: tuple[float, ...]#
station_names: tuple[str, ...]#
frequencies_hz: tuple[float, ...]#
components: tuple[str, ...]#
l2: float = 1.0#
dropout_probability()#

Return the fitted per-cell Bernoulli dropout probability.

Returns:

sigmoid(alpha + a_s + b_j + q_c), ready to pass to apply_structured_dropout().

Return type:

ndarray, shape (n_station, n_frequency, n_component)

Examples

>>> model = StructuredMissingness(
...     alpha=0.0, station_effect=(0.0,), frequency_effect=(0.0,),
...     component_effect=(0.0,), station_names=("A",),
...     frequencies_hz=(1.0,), components=("xy",),
... )
>>> model.dropout_probability()
array([[[0.5]]])
to_dict()#

Return a JSON-serializable representation.

Returns:

All fields as plain Python types, with a schema discriminator.

Return type:

dict

Examples

>>> model = StructuredMissingness(
...     alpha=0.0, station_effect=(0.0,), frequency_effect=(0.0,),
...     component_effect=(0.0,), station_names=("A",),
...     frequencies_hz=(1.0,), components=("xy",),
... )
>>> model.to_dict()["schema_version"]
1
classmethod from_dict(data)#

Restore a serialized model.

Parameters:

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

Returns:

Reconstructed, validated model.

Return type:

StructuredMissingness

Examples

>>> model = StructuredMissingness(
...     alpha=0.0, station_effect=(0.0,), frequency_effect=(0.0,),
...     component_effect=(0.0,), station_names=("A",),
...     frequencies_hz=(1.0,), components=("xy",),
... )
>>> StructuredMissingness.from_dict(model.to_dict()) == model
True
pycsamt.ai.domain_gap.fit_corruption_config(survey, *, severity_scale=1.0, components=None, noise_sampling='lognormal')#

Derive plausible noise/dropout ranges from a real survey’s QC.

Only quantities already present in the canonical SurveyData contract are used: the impedance_error-to-|Z| ratio for heteroscedastic noise and error floor, and coverage() for dropout rates.

Parameters:
  • survey (SurveyData) – Real (or realistically corrupted) survey to profile.

  • severity_scale (float, default=1.0) – Multiplier applied to every fitted range/rate, letting a caller derive a milder or harsher preset from the same empirical fit.

  • components (sequence of str, optional) – Impedance components whose error ratio calibrates the noise range and error floor, e.g. ("xy", "yx"). Calibrate on the components that will later be compared: diagonal components usually carry much larger relative errors, and including them inflates the fitted range for an off-diagonal comparison. None uses every component present. Dropout rates always use the full coverage mask.

  • noise_sampling ({"lognormal", "loguniform", "uniform"}, default="lognormal") – Stored on the returned configuration. The default treats the fitted range as the interquartile range of a log-normal error distribution, which reproduces the median and spread of declared field errors; "uniform" restores a flat draw between the quartiles.

Returns:

Configuration whose noise range spans the interquartile range of the observed relative error, whose error floor is the fifth percentile of that ratio, and whose dropout rates equal the observed missing fractions. Distortion and outlier parameters are left at zero; see fit_distortion_priors_from_sites() for those.

Return type:

CorruptionConfig

Raises:

ValueError – If survey has no declared impedance_error to profile.

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((4, 6, 2), 100 + 50j)
>>> err = np.full((4, 6, 2), 3.0)
>>> survey = SurveyData(
...     z,
...     np.linspace(1000, 1, 6),
...     ["A", "B", "C", "D"],
...     ["xy", "yx"],
...     np.zeros((4, 2)),
...     impedance_error=err,
... )
>>> config = fit_corruption_config(survey)
>>> config.noise_level_range[0] >= 0.0
True
pycsamt.ai.domain_gap.fit_distortion_priors_from_sites(sites, *, recursive=True, on_dup='replace', verbose=0, **kwargs)#

Estimate empirical static-shift and distortion spreads from real EDI.

This is the one M3 entry point that genuinely depends on the heavier, pandas-based EM diagnostics in pycsamt.emtools.gb and pycsamt.emtools.ss, run directly on real sites (e.g. a WILLY line) rather than on the numpy-only SurveyData contract.

Parameters:
  • sites (Any) – Anything accepted by pycsamt.emtools._core.ensure_sites().

  • recursive (bool) – Forwarded to the underlying diagnostics.

  • on_dup (str) – Forwarded to the underlying diagnostics.

  • verbose (int) – Forwarded to the underlying diagnostics.

  • **kwargs (Any) – Forwarded to pycsamt.emtools.gb.groom_bailey_table().

Returns:

static_shift_log10_sigma, distortion_gain_log10_sigma, distortion_twist_deg_sigma, distortion_shear_sigma, and distortion_anisotropy_sigma, each the population standard deviation of the corresponding per-station fitted parameter across stations with a successful fit. A parameter is 0.0 when fewer than two stations produced a usable fit.

Return type:

dict

Examples

>>> priors = fit_distortion_priors_from_sites(
...     "data/AMT/WILLY_DATA/L18PLT", recursive=False, verbose=0
... )
>>> sorted(priors)
['distortion_anisotropy_sigma', 'distortion_gain_log10_sigma', 'distortion_shear_sigma', 'distortion_twist_deg_sigma', 'static_shift_log10_sigma']
pycsamt.ai.domain_gap.fit_structured_missingness(survey, *, l2=1.0, max_iter=500)#

Fit a station+frequency+component logistic missingness model.

Replaces the three scalar dropout rates of fit_corruption_config() with an explicit model of where a survey’s missingness is concentrated. With Y_sjc = 1 - survey.valid[s, j, c] the observed missingness indicator and pi_sjc = sigmoid(alpha + a_s + b_j + q_c), the fit solves the penalized Bernoulli maximum-likelihood problem

\[\hat\vartheta = \arg\min_\vartheta \Bigl[ -\sum_{sjc}\bigl\{Y_{sjc}\log\pi_{sjc} +(1-Y_{sjc})\log(1-\pi_{sjc})\bigr\} +\lambda\bigl(\lVert a\rVert_2^2+\lVert b\rVert_2^2 +\lVert q\rVert_2^2\bigr)\Bigr],\]

with a, b, q each parameterized through a sum-to-zero contrast (_sum_zero_contrast()) so the fit is identified without an arbitrary reference level, optimized by L-BFGS-B using the closed-form gradient of the penalized negative log-likelihood (d(nll)/dz_sjc = pi_sjc - Y_sjc, the standard logistic-regression result).

Parameters:
  • survey (SurveyData) – Survey whose valid mask defines the real missingness pattern to fit. Should carry a genuine, disclosed missing-data mask (e.g. a coverage-aware, snap-or-missing survey), not one already interpolated/filled – interpolating over every gap removes the very signal this fit depends on.

  • l2 (float, default=1.0) – Ridge penalty weight \(\lambda\) on the station, frequency, and component effects (the intercept is not penalized).

  • max_iter (int, default=500) – Maximum L-BFGS-B iterations.

Returns:

Fitted model. Its dropout_probability() gives the full (n_station, n_frequency, n_component) probability field for apply_structured_dropout().

Return type:

StructuredMissingness

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.ones((3, 4, 1), dtype=complex)
>>> z[0, :2] = np.nan
>>> survey = SurveyData(
...     z, [4.0, 3.0, 2.0, 1.0], ["A", "B", "C"], ["xy"],
...     np.zeros((3, 2)),
... )
>>> model = fit_structured_missingness(survey)
>>> abs(sum(model.station_effect)) < 1e-6
True
>>> abs(sum(model.frequency_effect)) < 1e-6
True
pycsamt.ai.domain_gap.survey_data_from_sites(sites, *, crs=None, freq_rtol=1e-06, station_spacing=500.0, recursive=True, on_dup='replace', verbose=0, metadata=None)#

Bridge EDI/Sites/APISurvey input to canonical SurveyData.

Parameters:
  • sites (Any) – Anything accepted by pycsamt.emtools._core.ensure_sites(): a filesystem path/glob/directory, EDIFile/EDICollection, Site/Sites, APISurvey, or an iterable of these.

  • crs (str, optional) – Coordinate reference system identifier to record. Station positions are always projected with a local equirectangular approximation (see pycsamt.ai.inversion._sites_bridge.sites_to_coords_3d()); pass a CRS string only if it genuinely describes that projection.

  • freq_rtol (float, default=1e-6) – Relative tolerance used when checking that every station shares the same frequency grid.

  • station_spacing (float, default=500.0) – Forwarded to the coordinate bridge as a uniform-grid fallback spacing, used only when no station reports finite coordinates.

  • recursive (bool) – Forwarded to ensure_sites.

  • on_dup (str) – Forwarded to ensure_sites.

  • verbose (int) – Forwarded to ensure_sites.

  • metadata (dict, optional) – Extra provenance recorded on the returned survey.

Returns:

Canonical survey with full xx, xy, yx, yy components, with impedance and its declared error converted from Site’s EDI-native [mV/km]/[nT] convention to SI (V/A), matching SurveyData’s default ImpedanceConvention.

Return type:

SurveyData

Raises:

ValueError – If no station has usable impedance data, or stations do not share a common frequency grid within freq_rtol.

Notes

This function performs no frequency interpolation: a survey whose stations were sampled on different frequency grids must be resolved by an explicit, survey-matched frequency selector (an M1 concern) before reaching this bridge.

Examples

>>> survey = survey_data_from_sites(
...     "data/AMT/WILLY_DATA/L18PLT", recursive=False, verbose=0
... )
>>> survey.components
('xx', 'xy', 'yx', 'yy')
class pycsamt.ai.domain_gap.DistributionComparisonReport(comparisons)#

Bases: object

Collection of FeatureComparison results across features.

Parameters:

comparisons (mapping) – Feature name to FeatureComparison.

comparisons: Mapping[str, FeatureComparison]#
worst_feature()#

Return the feature name with the largest KS statistic.

Returns:

Feature name whose simulated/field distributions differ most, ignoring features with a NaN statistic (empty samples).

Return type:

str

Raises:

ValueError – If every feature has a NaN KS statistic.

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((2, 4, 1), 100 + 50j)
>>> survey = SurveyData(
...     z, np.linspace(100, 1, 4), ["A", "B"], ["xy"], np.zeros((2, 2))
... )
>>> report = compare_survey_distributions(survey, survey)
>>> report.worst_feature() in report.comparisons
True
to_dict()#

Return a JSON-serializable representation.

Returns:

Mapping of feature name to its comparison dict.

Return type:

dict

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((2, 4, 1), 100 + 50j)
>>> survey = SurveyData(
...     z, np.linspace(100, 1, 4), ["A", "B"], ["xy"], np.zeros((2, 2))
... )
>>> report = compare_survey_distributions(survey, survey)
>>> sorted(report.to_dict())
['error_to_magnitude_ratio', 'log_impedance_magnitude', 'phase_deg']
class pycsamt.ai.domain_gap.FeatureComparison(feature, simulated_stats, field_stats, ks_statistic, ks_pvalue, mean_difference, std_ratio)#

Bases: object

Quantitative comparison of one feature between two distributions.

Parameters:
  • feature (str) – Name of the compared feature.

  • simulated_stats (mapping) – count, mean, std, median of each sample.

  • field_stats (mapping) – count, mean, std, median of each sample.

  • ks_statistic (float) – Two-sample Kolmogorov-Smirnov statistic and p-value; NaN when either sample is empty.

  • ks_pvalue (float) – Two-sample Kolmogorov-Smirnov statistic and p-value; NaN when either sample is empty.

  • mean_difference (float) – simulated mean - field mean.

  • std_ratio (float) – simulated std / field std; NaN when the field std is zero.

feature: str#
simulated_stats: Mapping[str, float]#
field_stats: Mapping[str, float]#
ks_statistic: float#
ks_pvalue: float#
mean_difference: float#
std_ratio: float#
to_dict()#

Return a JSON-serializable representation.

Returns:

All fields, with statistics mappings converted to plain dicts.

Return type:

dict

Examples

>>> import numpy as np
>>> comparison = compare_feature_distributions(
...     np.array([1.0, 2.0, 3.0]),
...     np.array([1.0, 2.0, 3.0]),
...     feature="custom",
... )
>>> comparison.to_dict()["feature"]
'custom'
pycsamt.ai.domain_gap.compare_feature_distributions(simulated, field, *, feature)#

Compare two 1-D samples of the same feature quantitatively.

Parameters:
Returns:

Summary statistics, mean difference, std ratio, and two-sample KS test between the samples.

Return type:

FeatureComparison

Examples

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> comparison = compare_feature_distributions(
...     rng.normal(0, 1, 200), rng.normal(0, 1, 200), feature="demo"
... )
>>> comparison.ks_pvalue > 0.01
True
pycsamt.ai.domain_gap.compare_survey_distributions(simulated, field, *, features=('log_impedance_magnitude', 'phase_deg', 'error_to_magnitude_ratio'))#

Compare simulated and field surveys across several canonical features.

Parameters:
  • simulated (SurveyData) – Simulated (e.g. corrupted synthetic) survey.

  • field (SurveyData) – Real field survey, ideally sharing the simulated survey’s frequency band and component set for a meaningful comparison.

  • features (sequence of str, default features) – Any of "log_impedance_magnitude", "phase_deg", or "error_to_magnitude_ratio". The error-ratio feature is skipped (empty sample) for a survey without declared errors.

Returns:

One FeatureComparison per requested feature.

Return type:

DistributionComparisonReport

Examples

>>> import numpy as np
>>> from pycsamt.ai.data.contracts import SurveyData
>>> z = np.full((2, 4, 1), 100 + 50j)
>>> survey = SurveyData(
...     z, np.linspace(100, 1, 4), ["A", "B"], ["xy"], np.zeros((2, 2))
... )
>>> report = compare_survey_distributions(survey, survey)
>>> report.comparisons["phase_deg"].mean_difference
0.0

pycsamt.ai.domain_gap.audit

Survey-level audit reports for the M1 data-contract gate.

pycsamt.ai.domain_gap.simulator

Reproducible corruption of canonical surveys for domain-gap training.

pycsamt.ai.domain_gap.survey_fit

Fit plausible CorruptionConfig ranges from real data.

pycsamt.ai.domain_gap.report

Quantitative comparison between simulated and field feature distributions.

pycsamt.ai.domain_gap.empirical

Empirical field-calibrated corruption for synthetic EM surveys.