Source code for pycsamt.pipeline._smart_qc
"""Data-driven ("smart") QC plot gating for method-aware presets.
A plain :class:`~pycsamt.pipeline.StepSpec` QC hook is called as ``fn(sites)``
only (see :meth:`~pycsamt.pipeline.Step.generate_qc_plots`) — there is no
channel to pass extra parameters through. The wrappers in this module use
that single-argument contract to decide, from the data itself, whether a
plot is actually meaningful before calling it:
* Phase-tensor-ellipse plotting only makes sense once both off-diagonal
impedance components (Zxy and Zyx) are populated on at least one station
— a single-component TE/TM-only CSAMT line cannot produce a meaningful
ellipse.
* Tipper plots only make sense once a real (non-empty) vertical-field
channel is present on at least one station — most AMT surveys carry none.
Each wrapper returns ``None`` when its precondition isn't met.
:meth:`~pycsamt.pipeline.Step.generate_qc_plots` already treats a ``None``
return as "skip this plot, no figure saved, no error" — the same mechanism
every other QC hook relies on — so no changes to the step-execution engine
were needed to support this.
The detection helpers below (``_any_multicomponent``, ``_any_tipper``) reuse
the exact low-level idioms already used elsewhere in ``emtools`` for the
same checks (``_get_z_block``/direct Zxy·Zyx presence in
``fieldzone.py``/``source_effects.py``; ``_get_t_block`` in
``strike.py``/``inspect.py``), rather than the higher-level
``SiteMixin.has_component``/``quality_flags`` API, so behaviour matches what
``correct_near_field`` and ``plot_strike_analysis`` already consider
"present" on the exact same data.
"""
from __future__ import annotations
from typing import Any
import numpy as np
from ..emtools._core import _get_t_block, _get_z_block, _iter_items, ensure_sites
__all__ = [
"phase_tensor_smart",
"induction_multiperiod_map_smart",
"induction_section_smart",
"response_tipper_smart",
"tipper_components_smart",
"tipper_hodograms_smart",
]
def _any_multicomponent(sites: Any) -> bool:
"""True if any station has both Zxy and Zyx populated (finite, nonzero)."""
S = ensure_sites(sites, strict=False)
for ed in _iter_items(S):
_Z, z, _fr = _get_z_block(ed)
if z is None:
continue
zxy = np.abs(z[:, 0, 1])
zyx = np.abs(z[:, 1, 0])
if np.any((zxy > 0) & np.isfinite(zxy) & (zyx > 0) & np.isfinite(zyx)):
return True
return False
def _any_tipper(sites: Any) -> bool:
"""True if any station carries a real (non-empty) tipper channel."""
S = ensure_sites(sites, strict=False)
for ed in _iter_items(S):
_T, t, fr = _get_t_block(ed)
if t is None or fr is None:
continue
return True
return False
def _first_of(result: Any) -> Any:
"""Unwrap a ``(Figure, axes_array)``-style return down to the figure."""
if isinstance(result, tuple) and result:
return result[0]
return result
# ---------------------------------------------------------------------------
# Phase tensor — gated on multi-component data
# ---------------------------------------------------------------------------
[docs]
def phase_tensor_smart(sites: Any) -> Any:
"""``plot_phase_tensor_psection`` if the data is multi-component, else None.
A single-component TE/TM-only CSAMT line has only one off-diagonal Z
component populated, so a phase-tensor ellipse cannot be constructed
meaningfully — skip rather than draw a degenerate figure.
"""
if not _any_multicomponent(sites):
return None
from ..emtools.tensor import plot_phase_tensor_psection
return plot_phase_tensor_psection(sites)
# ---------------------------------------------------------------------------
# Tipper-dependent plots — gated on tipper presence
# ---------------------------------------------------------------------------
[docs]
def induction_multiperiod_map_smart(sites: Any) -> Any:
"""``plot_induction_multiperiod_map`` if tipper is present, else None."""
if not _any_tipper(sites):
return None
from ..emtools.tf import plot_induction_multiperiod_map
return _first_of(plot_induction_multiperiod_map(sites))
[docs]
def induction_section_smart(sites: Any) -> Any:
"""``plot_induction_section`` if tipper is present, else None."""
if not _any_tipper(sites):
return None
from ..emtools.tf import plot_induction_section
return plot_induction_section(sites)
[docs]
def response_tipper_smart(sites: Any) -> Any:
"""``plot_response_tipper`` if tipper is present, else None."""
if not _any_tipper(sites):
return None
from ..emtools.plot import plot_response_tipper
return plot_response_tipper(sites)
[docs]
def tipper_components_smart(sites: Any) -> Any:
"""``plot_tipper_components`` if tipper is present, else None."""
if not _any_tipper(sites):
return None
from ..emtools.inspect import plot_tipper_components
return plot_tipper_components(sites)
[docs]
def tipper_hodograms_smart(sites: Any) -> Any:
"""``plot_tipper_hodograms`` if tipper is present, else None."""
if not _any_tipper(sites):
return None
from ..emtools.tf import plot_tipper_hodograms
return plot_tipper_hodograms(sites)