2.25.3.18. pycsamt.ai.processing.imputer#
EMImputer — masked-reconstruction gap filling for MT impedance data.
Why this is not EMDenoiser#
EMDenoiser assumes every
(station, frequency) cell has some value and improves its quality.
Real surveys also have cells with no value at all – a frequency
dropped mid-acquisition, a component that never converged, a station
skipped for one band – and nothing else in pyCSAMT fills those gaps
with anything better than leaving them NaN through to inversion.
EMImputer targets that gap specifically: reconstructing the
missing cells from the observed ones, using spectral smoothness
(this station, nearby frequencies) – a matrix-completion /
masked-autoencoder problem, not a denoising one.
Architecture#
A self-supervised masked-reconstruction objective, the same family as BERT-style masked-language-modelling or image inpainting:
where \(\mathbf{m} \in \{0, 1\}\) is the mask fed to the network
(1 = “the network is told this cell is observed”) and
\(\mathbf{m}_\text{syn}\) marks the subset of genuinely observed
cells that were synthetically hidden this step, which is exactly where
the loss is evaluated. On every training step, EMImputer.fit()
hides a random mask_frac fraction of the truly-observed cells (any
real NaN gaps in the training data are always hidden and never
used as a target, since their true value is unknown) – mirroring the
way EMDenoiser adds synthetic
noise, so training data only needs to be mostly complete rather than
needing real missing-data examples with known ground truth.
The network is the same 1-D convolutional encoder-decoder family as
EMDenoiser (_build_cae_torch()
/ _build_cae_tf), with one structural difference: the encoder
takes 2 * n_components input channels (the zeroed-where-hidden
values, concatenated with the mask along the channel axis) so the
network can distinguish “zero because hidden” from “zero because that
is genuinely the value”, while the decoder still only reconstructs
n_components channels – the mask itself is never reconstructed.
Input tensor shape#
(n_samples, n_components, n_freqs), identical to
prepare_z_features()’s output –
NaN marks a missing cell.
Working directly with site collections#
EMImputer.apply() accepts and returns a site collection
directly, writing the reconstructed impedance back only into cells
that were genuinely missing on the input – every observed measurement
is left byte-identical – the same sites-in / sites-out convention as
apply().
Classes
|
Masked-reconstruction gap filler for MT impedance tensor data. |
- class pycsamt.ai.processing.imputer.EMImputer(n_freqs=None, n_components=4, *, channels=(64, 128, 64), dropout=0.1, device=None)[source]
Bases:
BaseEMProcessorMasked-reconstruction gap filler for MT impedance tensor data.
Trains a 1-D convolutional autoencoder to reconstruct synthetically-hidden cells of otherwise-observed impedance spectra, then applies that network to fill genuinely missing (
NaN) cells at inference time. Supports both PyTorch and TensorFlow backends viapycsamt.backends.- Parameters:
n_freqs (int or None, default None) – Number of frequency channels in the input. When
None, inferred fromX.shape[2]on the first call tofit().n_components ({4, 8}, default 4) – Feature channels per frequency, matching
prepare_z_features().channels (tuple of int, default (64, 128, 64)) – Channel widths for the encoder/decoder stages.
dropout (float, default 0.1) – Dropout probability in the encoder bottleneck.
device (str or None) –
"cpu","cuda","mps","/GPU:0", orNonefor auto-detect via the active backend.
Notes
When neither PyTorch nor TensorFlow is available the imputer falls back to per-sample, per-channel linear interpolation along the frequency axis (
_freq_axis_fill()) – no spatial (cross- station) information, unlike the network path.Call
prepare_z_features()to convert a site collection to the(n_samples, n_components, n_freqs)array expected here, or useapply()to go straight from a site collection to a filled one without handling the array form yourself.Examples
>>> import numpy as np >>> X = np.random.randn(200, 4, 32).astype("float32") >>> rng = np.random.default_rng(0) >>> gaps = rng.random(X.shape) < 0.05 >>> X[gaps] = np.nan >>> imp = EMImputer() >>> imp.fit(X, epochs=5, verbose=False) EMImputer(n_freqs=32, n_components=4, fitted) >>> X_filled = imp.transform(X)
Sites in, filled sites out – the same convention as
apply():>>> complete_sites = imp.apply(sites)
- fit(X, *, mask_frac=0.15, epochs=80, batch_size=64, lr=0.001, val_frac=0.1, seed=None, verbose=True)[source]
Train the imputer on a (mostly) complete impedance array.
- Parameters:
X (ndarray, shape (n_samples, n_components, n_freqs)) – Training data.
NaNcells, if any, are excluded from the reconstruction loss – only cells that are genuinely observed can be synthetically hidden and used as a self-supervised target.mask_frac (float, default 0.15) – Fraction of the observed cells to hide on every training step, supplying the reconstruction target (see the module docstring). Too low and the network sees little missing- data signal to learn from; too high and too little context remains for it to reconstruct anything.
epochs (int) – Maximum number of training epochs.
batch_size (int)
lr (float) – Initial Adam learning rate.
val_frac (float) – Fraction of training samples held out for validation.
seed (int or None)
verbose (bool)
- Return type:
self
- Raises:
ValueError – If
Xis not 3-D, its frequency count does not match a previously-fittedn_freqs, or it contains no finite values at all.
- transform(X)[source]
Fill missing (
NaN) cells inX.- Parameters:
X (ndarray, shape (n_samples, n_components, n_freqs)) – Data with
NaNmarking missing cells.- Returns:
X_filled – Observed cells returned unchanged (byte-identical to the input); missing cells replaced by the network’s reconstruction.
- Return type:
ndarray, same shape as input
- apply(sites, *, freq_ref=None, log_amp=True, inplace=False, recursive=True, on_dup='replace', strict=False, verbose=0)[source]
Fill a site collection’s missing impedance cells in place.
Extracts features with
prepare_z_features(), imputes them withtransform(), then writes the reconstruction back onto each site’s own frequency grid – but only at frequency rows that were genuinely missing on that site to begin with; every observed row is left byte-identical to the input. Mirrorsapply()’s sites-in / sites-out convention.- Parameters:
sites (SiteCollection or compatible)
freq_ref (ndarray or None) – Common frequency grid (Hz) the network was trained on. Must match the grid used to prepare the training data – when
None(the default), the first site’s own grid is used, matchingprepare_z_features()’s own default.log_amp (bool, default
True) – Must match thelog_ampused to build the training features.inplace (bool, default
False) – WhenFalse(the default), sites is left untouched and a filled copy is returned. WhenTrue, sites is modified in place and returned.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:
filled – A site collection with the same stations. Stations with no missing cells at all are returned untouched.
- Return type:
- Warns:
With the default ``n_components=4``, only :math:`Z_{xy}` and
:math:`Z_{yx}` are modelled – a real missing measurement
usually means the *whole* frequency row (all four components)
is absent, and the diagonal :math:`Z_{xx}, Z_{yy}` at those
rows is left exactly as it was (``NaN``, if it was ``NaN``),
the same “not modelled -> untouched” contract
:meth:`~pycsamt.ai.processing.denoise.EMDenoiser.apply` uses.
Fit and apply with ``n_components=8`` to fill every component.
- property history_: dict[str, list][source]
Training history recorded by the last
fit()call.- Returns:
history –
{"train_loss": [...], "val_loss": [...]}, one value per epoch, both computed only over synthetically-hidden cells (see the module docstring). Empty when the frequency- interpolation fallback was used (no epoch loop) or beforefit()has been called. Pass directly toplot_training_history().- Return type: