2.25.3.22. pycsamt.ai.processing.uncertainty#

UncertaintyCalibrator — learned per-cell error-floor re-estimation.

Why this is not EMQCScorer#

EMQCScorer answers “is this (station, frequency) cell trustworthy?” with a score in [0, 1]. It does not answer “how large should the error bar on this cell be?” – yet that second question is exactly what an inversion error floor needs, and today pyCSAMT (like most MT software) uses whatever z_err field the processing software wrote, or a flat percentage-of-|Z| floor. Both are frequently miscalibrated: field error propagation is often too optimistic in quiet bands and too pessimistic near noise spikes. UncertaintyCalibrator re-estimates that error directly, using the same per-cell diagnostic features EMQCScorer already extracts (SNR, Swift skew, off-diagonal amplitude asymmetry, both off-diagonal phases) as regression inputs instead of classification inputs.

Training target: recalibration mode#

There is no pre-existing “correct error bar” to regress against, the way EMQCScorer’s rule-based labels or DimensionalityClassifier’s skew/ellipticity thresholds exist. Two options were considered:

  1. Recalibration mode (implemented here) – train against the field-processing z_err itself, expressed as a fractional error \(|z_\text{err}| / |Z|\) rather than an absolute value, so the target is comparable across stations and frequencies with very different \(|Z|\) (the raw z_err column on L18PLT spans three decades, 1.3 to 1625, purely from \(|Z|\) scale; the fractional error is a tight, physically meaningful 1.1% to 54%, median 4.9%, genuinely varying across 1428 distinct values – confirmed non-degenerate before committing to it, the same check that caught Groom-Bailey’s always-1.0 gain column being unsafe as a feature in DistortionTypeClassifier). The network is not memorising z_err verbatim – it only sees the five diagnostic features, so its output is a smoothed, internally consistent re-estimate driven by signal quality, not a pass-through copy.

  2. Empirical mode – train against a genuine bootstrap or repeat-measurement spread computed from raw cross-power spectra (pycsamt.seg.spectra) when available, rather than a recalibrated copy of the field estimate. Not implemented here: none of pyCSAMT’s bundled field-survey EDI data carries an >SPECTRA section (only four small vendor demo files under data/MT/SPECTRA/ do, with placeholder COUNTRY="EXAMPLE" metadata – not a real profile, so not a fair verification target for this package’s real-data standard). fit() still accepts any y explicitly, so an empirical target computed independently – from pycsamt.seg.spectra, or anywhere else – plugs into the exact same regressor without an API change; only the automatic y=None default is recalibration-mode.

Feature vector (per station-frequency cell)#

[swift_skew, asym, phase_xy, phase_yx] – four of EMQCScorer’s five features, built by the same _extract_qc_features().

Note

EMQCScorer’s fifth feature, snr, is deliberately not used as a model input here, even though build_uncertainty_features_table() still reports it as a diagnostic column. snr = amp / (z_err + eps) and the recalibration target z_err_frac = z_err / (amp + eps) are built from the exact same pooled amp/z_err terms – verified on real L18PLT data, corr(1/snr, z_err_frac) = 1 - 2e-16, i.e. identical to floating-point precision. Feeding snr in would let the network “recalibrate” by trivially inverting one input column rather than genuinely re-estimating error from signal shape – a target-leakage pitfall (a feature that deterministically encodes the answer), distinct from the degenerate-feature pitfall caught in DistortionTypeClassifier (its original, always-1.0 gain feature carried no information at all), but both were caught the same way – by checking a candidate feature’s actual behaviour against real data before trusting it.

build_uncertainty_features_table() builds this table (plus the z_err_frac recalibration target) directly from a site collection.

Working directly with site collections#

UncertaintyCalibrator.apply() accepts and returns a site collection, sites-in / sites-out like apply() – but it rescales Z.z_err rather than replacing it outright, preserving whatever per-component structure the field error already has (e.g. a larger diagonal-term error) while shifting the overall level to match the calibrated estimate.

Functions

build_uncertainty_features_table(sites, *[, ...])

Build the per-(station, frequency) feature table UncertaintyCalibrator expects.

Classes

UncertaintyCalibrator([n_features, hidden, ...])

Learned per-(station, frequency) uncertainty / error-floor estimator.

class pycsamt.ai.processing.uncertainty.UncertaintyCalibrator(n_features=4, *, hidden=(64, 32), dropout=0.1, lr=0.001, device=None)[source]

Bases: BaseEMProcessor

Learned per-(station, frequency) uncertainty / error-floor estimator.

Parameters:
  • n_features (int, default 4) – Length of the per-cell feature vector. fit()/ transform() on a build_uncertainty_features_table() DataFrame always use the 4-column [swift_skew, asym, phase_xy, phase_yx] layout – deliberately excluding snr (see the module docstring); raise this only when fitting on a custom feature matrix with more columns of your own.

  • hidden (tuple of int, default (64, 32)) – Hidden-layer widths of the regressor backbone.

  • dropout (float, default 0.1) – Dropout probability in each hidden layer.

  • lr (float, default 1e-3) – Default learning rate; can be overridden in fit().

  • device (str or None) – "cpu", "cuda", "mps", "/GPU:0", or None for auto-detect via the active backend.

Notes

Falls back to a random-forest regressor (scikit-learn) when neither PyTorch nor TensorFlow is available.

Examples

>>> from pycsamt.ai.processing.uncertainty import (
...     UncertaintyCalibrator, build_uncertainty_features_table,
... )
>>> feats = build_uncertainty_features_table(sites)
>>> cal = UncertaintyCalibrator.from_features_table(
...     feats, epochs=150,
... )
>>> table = cal.predict_table(sites)
classmethod from_features_table(df, *, log_target=True, **fit_kwargs)[source]

Construct and train a calibrator from a build_uncertainty_features_table() DataFrame.

Parameters:
  • df (DataFrame) – Must contain a z_err_frac column (self-training recalibration target); pass y= directly to fit() instead for an empirical/bootstrap target.

  • log_target (bool, default True) – Train in log10 space (recommended – fractional error is right-skewed); transform() always returns the linear-space value regardless.

  • **fit_kwargs – Passed to fit() (e.g. epochs=150).

Return type:

UncertaintyCalibrator

fit(X, y=None, *, log_target=True, epochs=150, batch_size=64, lr=None, val_frac=0.15, seed=None, verbose=True)[source]

Train the uncertainty calibrator.

Parameters:
  • X (ndarray (n_samples, n_features) or DataFrame) – A DataFrame from build_uncertainty_features_table() self-trains against its own z_err_frac column when y is not given.

  • y (ndarray (n_samples,) or None) – Target error/uncertainty values, any positive-valued scale (recalibration fractional error, an empirical bootstrap spread, …). Required when X is a plain ndarray – there is no rule to synthesise a numeric target from features alone the way the classifiers in this package synthesise class labels.

  • log_target (bool, default True) – Train in log10 space. transform() output is always de-logged back to the original target’s linear scale.

  • epochs (int) – Training hyper-parameters.

  • batch_size (int) – Training hyper-parameters.

  • lr (float | None) – Training hyper-parameters.

  • val_frac (float) – Training hyper-parameters.

  • seed (int | None) – Training hyper-parameters.

  • verbose (bool) – Training hyper-parameters.

Return type:

self

Raises:

ValueError – y is None and no target can be recovered from X (see above), or no finite training rows remain.

transform(X)[source]

Predict calibrated per-cell uncertainty.

Parameters:

X (ndarray (n_samples, n_features) or DataFrame)

Returns:

y_pred – Calibrated error/uncertainty estimate, de-logged back to the same linear scale as the fit() target (e.g. fractional error, in [0, 1]-ish units, for the default recalibration target).

Return type:

ndarray, shape (n_samples,)

predict_table(sites, *, recursive=True, on_dup='replace', strict=False, verbose=0)[source]

Calibrate uncertainty for an entire site collection.

Parameters:
  • sites (SiteCollection or compatible)

  • recursive (bool) – Passed to ensure_sites().

  • on_dup (str) – Passed to ensure_sites().

  • strict (bool) – Passed to ensure_sites().

  • verbose (int) – Passed to ensure_sites().

Returns:

df – build_uncertainty_features_table()’s columns plus z_err_frac_calibrated.

Return type:

DataFrame

apply(sites, *, inplace=False, floor_frac=None, recursive=True, on_dup='replace', strict=False, verbose=0)[source]

Rescale Z.z_err to the calibrated level, sites-in / sites-out.

Unlike apply(), this does not replace z_err outright: it computes the ratio between the calibrated fractional error and the station’s own original pooled fractional error at each frequency, then scales all four tensor components’ existing z_err by that one ratio – preserving whatever per-component structure the field error already carries (e.g. a larger diagonal-term error) while shifting the overall level to the calibrated estimate.

Parameters:
  • sites (SiteCollection or compatible)

  • inplace (bool, default False) – When False (the default), sites is left untouched and a corrected copy is returned. When True, sites is modified in place and returned.

  • floor_frac (float or None, default None) – Minimum fractional error to enforce after calibration (e.g. 0.02 for a 2% floor) – applied with np.maximum after the network prediction, before rescaling. None applies no floor.

  • recursive (bool) – Passed to ensure_sites().

  • on_dup (str) – Passed to ensure_sites().

  • strict (bool) – Passed to ensure_sites().

  • verbose (int) – Passed to ensure_sites().

Returns:

corrected – A site collection with the same stations; Z.z_err is rescaled station-by-station, frequency-by-frequency. Stations with no usable z_err to begin with are left untouched (there is nothing to rescale from).

Return type:

Sites

Examples

>>> cal = UncertaintyCalibrator().fit(feats)
>>> calibrated = cal.apply(sites)
property history_: dict[str, list][source]

Training history recorded by the last fit() call.

Returns:

history – {"train_loss": [...], "val_loss": [...]}, one value per epoch, in normalised log-target loss units. Empty when the random-forest fallback was used (no epoch loop) or before fit() has been called. Pass directly to plot_training_history().

Return type:

dict

pycsamt.ai.processing.uncertainty.build_uncertainty_features_table(sites, *, recursive=True, on_dup='replace', strict=False, verbose=0)[source]

Build the per-(station, frequency) feature table UncertaintyCalibrator expects.

Parameters:
  • sites (SiteCollection or compatible)

  • recursive (bool) – Passed to ensure_sites().

  • on_dup (str) – Passed to ensure_sites().

  • strict (bool) – Passed to ensure_sites().

  • verbose (int) – Passed to ensure_sites().

Returns:

df – Columns: station, freq, snr, swift_skew, asym, phase_xy, phase_yx (identical to EMQCScorer’s feature table – snr is reported for inspection but is not one of UncertaintyCalibrator’s own model inputs, see the module docstring) plus z_err_frac – the field-processing fractional error \(|z_\text{err}|/|Z|\), pooled the same way as the snr column’s own numerator/denominator. Rows whose site has no z_err at all are dropped (z_err_frac cannot be computed); empty (but correctly columned) when no station has error estimates.

Return type:

DataFrame