Source code for pycsamt.agents.qc

"""
pycsamt.agents.qc
=================

:class:`DataQCAgent` — MT data quality control and frequency editing.

Wraps :mod:`pycsamt.emtools.qc`:

* :func:`~pycsamt.emtools.qc.build_qc_table`        — per-station metrics
* :func:`~pycsamt.emtools.qc.qc_flags`               — pass / fail flags
* :func:`~pycsamt.emtools.qc.frequency_confidence_table` — per-frequency scores
* :func:`~pycsamt.emtools.qc.plot_frequency_confidence_psection` — section figure
* :func:`~pycsamt.emtools.qc.plot_confidence_profile`            — profile figure
* :func:`~pycsamt.emtools.qc.station_confidence_table`           — per-station confidence

Output figures use :data:`~pycsamt.api.section.PYCSAMT_SECTION` so they are
consistent with all other pycsamt plots.
"""

from __future__ import annotations

import time
from typing import Any

from ._base import AgentResult, BaseAgent

_SYSTEM_PROMPT = """\
You are an expert MT/AMT/CSAMT data quality analyst for pycsamt v2.
Given a survey QC summary, write 3–4 sentences that:
1. State the overall data quality (good / moderate / poor).
2. Identify specific stations or frequency bands that need attention.
3. Explain the likely cause (instrument noise, EM interference, near-field).
4. Recommend the most important next processing step.
Reply in plain English. No bullet points, no markdown headings.
"""


[docs] class DataQCAgent(BaseAgent): """Run data quality control on a MT/AMT dataset. Parameters ---------- api_key, model, llm_provider : str LLM configuration (optional). method : str Confidence scoring method: ``"composite"`` (default), ``"presence"``, ``"snr"``, or ``"spatial"``. min_frac_ok : float Minimum fraction of OK frequencies for a station to pass (0–1). min_snr_med : float Minimum median SNR for a station to pass. max_skew_med : float Maximum median |β| skewness for a station to pass. Input keys ---------- ``sites`` : Sites or ``path`` : str EDI data to assess. ``output_dir`` : str, optional Where to save QC figures. ``period_range`` : [T_min, T_max], optional Restrict QC to this period window. Output data keys ---------------- ``qc_table`` pandas DataFrame — per-station metrics ``flags`` pandas DataFrame — pass / fail per station ``confidence_table`` pandas DataFrame — per-station confidence scores ``freq_conf_table`` pandas DataFrame — per-frequency confidence ``n_flagged`` int ``flagged_stations`` list[str] ``figures`` dict — matplotlib Figure objects ``figure_paths`` dict — saved file paths (when output_dir set) Examples -------- >>> agent = DataQCAgent() >>> result = agent.execute({"path": "/data/L22PLT", ... "output_dir": "/out/qc"}) >>> result["n_flagged"] 2 >>> result["figures"]["confidence_section"] <Figure …> """ SYSTEM_PROMPT = _SYSTEM_PROMPT def __init__( self, *, api_key: str | None = None, model: str | None = None, llm_provider: str = "claude", method: str = "composite", min_frac_ok: float = 0.6, min_snr_med: float = 2.0, max_skew_med: float = 6.0, ) -> None: super().__init__( "DataQCAgent", api_key=api_key, model=model, llm_provider=llm_provider, section_preset="pseudosection", ) self.method = method self.min_frac_ok = min_frac_ok self.min_snr_med = min_snr_med self.max_skew_med = max_skew_med
[docs] def execute(self, input_data: dict[str, Any]) -> AgentResult: self._last_cost = 0.0 t0 = time.time() warnings: list[str] = [] # ── resolve sites ───────────────────────────────────────────────────── from ..emtools._core import ensure_sites sites_raw = input_data.get("sites") or input_data.get("path") if sites_raw is None: return AgentResult.failed( "No 'sites' or 'path' key in input_data.", elapsed=time.time() - t0, ) try: sites = ensure_sites(sites_raw, verbose=0) except Exception as exc: return AgentResult.failed(str(exc), elapsed=time.time() - t0) output_dir = input_data.get("output_dir") # ── import emtools.qc functions ─────────────────────────────────────── from ..emtools.qc import ( build_qc_table, frequency_confidence_table, plot_confidence_profile, plot_frequency_confidence_psection, qc_flags, station_confidence_table, ) # ── build tables ────────────────────────────────────────────────────── qc_table = conf_table = freq_conf = flags_df = None try: qc_table = build_qc_table(sites, verbose=0) except Exception as exc: warnings.append(f"build_qc_table: {exc}") try: flags_df = qc_flags( sites, min_frac_ok=self.min_frac_ok, min_snr_med=self.min_snr_med, max_skew_med=self.max_skew_med, verbose=0, ) except Exception as exc: warnings.append(f"qc_flags: {exc}") try: conf_table = station_confidence_table( sites, method=self.method, verbose=0 ) except Exception as exc: warnings.append(f"station_confidence_table: {exc}") try: freq_conf = frequency_confidence_table( sites, method=self.method, verbose=0 ) except Exception as exc: warnings.append(f"frequency_confidence_table: {exc}") # ── flagged stations ────────────────────────────────────────────────── flagged: list[str] = [] if flags_df is not None and hasattr(flags_df, "iterrows"): for _, row in flags_df.iterrows(): if not bool(row.get("pass", True)): flagged.append(str(row.get("station", ""))) n_flagged = len(flagged) # ── figures ─────────────────────────────────────────────────────────── figures: dict[str, Any] = {} fig_paths: dict[str, str] = {} # confidence pseudosection try: ax_conf = plot_frequency_confidence_psection( sites, method=self.method, section="pseudosection", verbose=0, ) fig = ( ax_conf.get_figure() if hasattr(ax_conf, "get_figure") else ax_conf ) figures["confidence_section"] = fig p = self._save_figure( fig, output_dir, "qc_confidence_section", warnings_list=warnings, ) if p: fig_paths["confidence_section"] = p except Exception as exc: warnings.append(f"plot_frequency_confidence_psection: {exc}") # confidence profile (per-station score bar) try: ax_prof = plot_confidence_profile( sites, method=self.method, verbose=0 ) fig = ( ax_prof.get_figure() if hasattr(ax_prof, "get_figure") else ax_prof ) figures["confidence_profile"] = fig p = self._save_figure( fig, output_dir, "qc_confidence_profile", warnings_list=warnings, ) if p: fig_paths["confidence_profile"] = p except Exception as exc: warnings.append(f"plot_confidence_profile: {exc}") # ── LLM interpretation ──────────────────────────────────────────────── interp: str | None = None if self.api_key: n_st = len(qc_table) if qc_table is not None else "?" prompt = ( f"Survey QC summary:\n" f" Stations evaluated: {n_st}\n" f" Flagged (fail): {n_flagged}{flagged}\n" f" Warnings: {warnings[:4] if warnings else 'none'}\n" f" Method: {self.method}\n\n" "Assess data quality and recommend the next processing step." ) interp = self.query_llm(prompt, max_tokens=200) elapsed = time.time() - t0 status = ( "success" if n_flagged == 0 else ("needs_review" if n_flagged < 5 else "needs_review") ) return AgentResult( status=status, summary=( f"QC complete: {n_flagged} station(s) flagged out of " f"{len(qc_table) if qc_table is not None else '?'}. " f"{len(figures)} figure(s) produced." ), data={ "qc_table": qc_table, "flags": flags_df, "confidence_table": conf_table, "freq_conf_table": freq_conf, "n_flagged": n_flagged, "flagged_stations": flagged, "figures": figures, "figure_paths": fig_paths, "sites": sites, }, warnings=warnings, llm_interpretation=interp, elapsed_seconds=elapsed, cost_estimate_usd=self._last_cost, )
__all__ = ["DataQCAgent"]