Source code for pycsamt.pipeline._presets

"""Named pipeline presets for common MT processing workflows.

Each preset is a :class:`Preset` object that pairs a description with an
ordered list of :class:`~pycsamt.emtools.pipe._steps.Step` instances.  Users
can load a preset via :meth:`~pycsamt.emtools.pipe.Pipeline.from_preset` and
then add, remove, or override steps as needed.

Available presets
-----------------
``"basic_qc"``
    Minimal denoising + frequency cleanup.  Good for a first-pass inspection.
``"noise_reduction"``
    Stacked noise-removal chain for high-EMI environments.
``"full_processing"``
    Standard end-to-end workflow: noise → frequency → skew gate → SS → strike.
``"tensor_analysis"``
    Tensor-only cleanup (rotation, antisymmetry, sigma-clip, balance).
``"dimensionality_filter"``
    Classify, mask, and project to 2-D.
``"publication_ready"``
    Full chain suitable for Computers & Geosciences quality output.
``"stratagem_mt"``
    emtools-level preset for Stratagem AMT data already loaded as Sites.
    Static-shift → AMT band select → powerline notch → Hampel → QC.
    Use :mod:`pycsamt.pipeline.stratagem` when you also need coordinate
    injection and renaming from raw EDI + CSV inputs.
"""

from __future__ import annotations

from dataclasses import dataclass, field

from ._steps import Step

# ---------------------------------------------------------------------------
# Preset dataclass
# ---------------------------------------------------------------------------


[docs] @dataclass class Preset: """A named, ordered collection of pipeline steps. Attributes ---------- name: Identifier used in :meth:`~pycsamt.emtools.pipe.Pipeline.from_preset`. description: One-line description shown when the user prints the preset catalogue. steps: Ordered list of ``(label, Step)`` tuples — same format as a :class:`~pycsamt.emtools.pipe.Pipeline` step list. """ name: str description: str steps: list[tuple[str, Step]] = field(default_factory=list) def __repr__(self) -> str: codes = " → ".join(s.spec.code for _, s in self.steps) return f"Preset({self.name!r} [{codes}])"
# --------------------------------------------------------------------------- # Preset definitions # --------------------------------------------------------------------------- PRESETS: dict[str, Preset] = { "basic_qc": Preset( name="basic_qc", description=( "Minimal denoising + frequency cleanup. " "Good for quick-look inspection." ), steps=[ ("notch", Step("NR001")), ("drop_duplicates", Step("FREQ002")), ("select_band", Step("FREQ001")), ("align_grid", Step("FREQ004")), ("qc_snapshot", Step("QC001")), ], ), "noise_reduction": Preset( name="noise_reduction", description=( "Stacked noise-removal chain for high-EMI environments." ), steps=[ ("notch", Step("NR001")), ("hampel", Step("NR004")), ("spatial_med", Step("NR005")), ("shrink_trend", Step("NR003")), ("mask_incoher", Step("NR010")), ("qc_snapshot", Step("QC001")), ], ), "full_processing": Preset( name="full_processing", description=( "Standard end-to-end workflow: " "noise → frequency → skew gate → static-shift → strike rotation." ), steps=[ ("notch", Step("NR001")), ("drop_dup", Step("FREQ002")), ("select_band", Step("FREQ001")), ("align_grid", Step("FREQ004")), ("mask_skew", Step("SK001")), ("rotate_strike", Step("TZ001")), ("correct_ss", Step("SS001")), ("qc_snapshot", Step("QC001")), ], ), "tensor_analysis": Preset( name="tensor_analysis", description=( "Tensor-only cleanup: strike rotation, antisymmetry, " "sigma-clip, and off-diagonal balance." ), steps=[ ("rotate_strike", Step("TZ001")), ("antisymm", Step("TZ002")), ("sigma_clip", Step("TZ003")), ("balance", Step("TZ004")), ("qc_snapshot", Step("QC001")), ], ), "dimensionality_filter": Preset( name="dimensionality_filter", description=( "Classify 1-D / 2-D / 3-D regions, mask unwanted class, " "and project to 2-D." ), steps=[ ("classify_dim", Step("DIM001")), ("mask_dim", Step("DIM002")), ("project_2d", Step("DIM003")), ("qc_snapshot", Step("QC001")), ], ), "publication_ready": Preset( name="publication_ready", description=( "Full chain for publication-quality output " "(C&G paper standard): noise removal, frequency editing, " "static-shift correction, strike rotation, and skew gating." ), steps=[ ("notch", Step("NR001")), ("drop_dup", Step("FREQ002")), ("select_band", Step("FREQ001")), ("align_grid", Step("FREQ004")), ("correct_ss", Step("SS001")), ("rotate_strike", Step("TZ001")), ("antisymm", Step("TZ002")), ("mask_skew", Step("SK001")), ("qc_snapshot", Step("QC001")), ], ), # ── Stratagem-specific preset ───────────────────────────────────────── "stratagem_mt": Preset( name="stratagem_mt", description=( "emtools-level preset for Stratagem AMT data already loaded " "as a Sites object. Applies AMA static-shift correction, " "selects the standard AMT band (10 Hz – 100 kHz), removes " "powerline harmonics and outliers, and masks incoherent " "frequency bins. For the full raw EDI + GPS workflow " "(coordinate injection, hardware SNR masking, renaming) use " "pycsamt.pipeline.stratagem.StratagemPipeline instead." ), steps=[ # static shift first — requires complete frequency range ("correct_ss", Step("SS001")), # trim to AMT acquisition band ("select_band", Step("FREQ001", band_hz=(10.0, 1e5))), # drop duplicate frequencies from multi-band acquisition ("drop_dup", Step("FREQ002")), # powerline harmonics (50 Hz grid, 30 harmonics covers AMT range) ("notch", Step("NR001", mains_hz=50.0, n_harm=30, tol_hz=0.08)), # Hampel outlier filter on impedance tensor ("hampel", Step("NR004")), # mask frequencies that are incoherent across the profile ("mask_incoher", Step("NR010")), ("qc_snapshot", Step("QC001")), ], ), } # --------------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------------
[docs] def list_presets() -> list[Preset]: """Return all available :class:`Preset` objects.""" return list(PRESETS.values())
[docs] def get_preset(name: str) -> Preset: """Return the :class:`Preset` for *name*. Raises ------ KeyError When *name* is not registered. """ if name not in PRESETS: available = sorted(PRESETS) raise KeyError( f"Unknown preset {name!r}. Available presets: {available}" ) return PRESETS[name]
[docs] def preset_catalogue() -> str: """Return a formatted catalogue of all presets.""" lines = ["Available pipeline presets", "─" * 60] for p in PRESETS.values(): codes = " → ".join(s.spec.code for _, s in p.steps) lines.append(f" {p.name:<22s} {p.description}") lines.append(f" {'':22s} {codes}") lines.append("") return "\n".join(lines)
__all__ = [ "Preset", "PRESETS", "get_preset", "list_presets", "preset_catalogue", ]