Source code for pycsamt.agents.inversion_prep

"""
pycsamt.agents.inversion_prep
==============================

:class:`InversionPrepAgent` — Prepare Occam2D / ModEM data files.

Phase 2 stub: validates the Sites object and writes inversion-ready
data files in the requested format.  Full Occam2D write support
uses pycsamt's existing model writers; ModEM is planned for Phase 3.
"""

from __future__ import annotations

import os
import time
from typing import Any

from ._base import AgentResult, BaseAgent

_SYSTEM_PROMPT = """\
You are an expert MT inversion specialist.
Given a dataset summary and chosen inversion code, recommend:
1. Appropriate period band and error floor.
2. Mesh geometry (cell sizes, padding, depth extent).
3. Regularisation starting values.
4. Any pre-processing steps still needed before inversion.
Reply in 4–5 sentences, plain English.
"""


[docs] class InversionPrepAgent(BaseAgent): """Prepare MT data files for 2-D / 3-D inversion codes. Currently supported output formats ----------------------------------- * ``"occam2d"`` — Occam2D DataFile format * ``"modem"`` — ModEM3D data file (Phase 3) Input keys ---------- ``sites`` / ``path`` : Sites or str ``code`` : str — ``"occam2d"`` (default) or ``"modem"`` ``period_range`` : [T_min, T_max], optional ``component`` : str — ``"xy"``, ``"yx"``, or ``"both"`` ``error_floor`` : float — minimum relative error floor (default 0.05) ``output_dir`` : str Output data keys ---------------- ``data_file_path`` str — path to the written data file ``n_periods`` int ``n_stations`` int ``code`` str """ SYSTEM_PROMPT = _SYSTEM_PROMPT def __init__( self, *, api_key: str | None = None, model: str | None = None, llm_provider: str = "claude", code: str = "occam2d", error_floor: float = 0.05, ) -> None: super().__init__( "InversionPrepAgent", api_key=api_key, model=model, llm_provider=llm_provider, ) self.code = code self.error_floor = error_floor
[docs] def execute(self, input_data: dict[str, Any]) -> AgentResult: self._last_cost = 0.0 t0 = time.time() warnings: list[str] = [] from ..emtools._core import ( _get_z_block, _iter_items, 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'.", 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) code = str(input_data.get("code", self.code)).lower() output_dir = input_data.get("output_dir", "pycsamt_inversion_prep") per_range = input_data.get("period_range") component = str(input_data.get("component", "both")).lower() err_floor = float(input_data.get("error_floor", self.error_floor)) os.makedirs(output_dir, exist_ok=True) # count stations + periods n_st = 0 per_all = [] import numpy as np for _i, ed in enumerate(_iter_items(sites)): n_st += 1 Z_obj, z, fr = _get_z_block(ed) if fr is not None: per = 1.0 / np.where(fr == 0, np.nan, fr) if per_range: mask = (per >= per_range[0]) & (per <= per_range[1]) per = per[mask] per_all.extend(per[np.isfinite(per)].tolist()) n_per = len(set(f"{p:.4e}" for p in per_all)) data_file_path: str | None = None if code == "occam2d": try: from ..models.occam2d import ( write_occam2d_data, ) out_path = os.path.join(output_dir, "OccamDataFile.dat") write_occam2d_data( sites, out_path, period_range=per_range, component=component, error_floor=err_floor, ) data_file_path = out_path except ImportError: warnings.append( "pycsamt.models.occam2d not available. " "Data file was not written." ) except Exception as exc: warnings.append(f"write_occam2d_data failed: {exc}") elif code == "modem": warnings.append( "ModEM data file writer is planned for Phase 3. " "No file was written." ) else: warnings.append(f"Unknown inversion code {code!r}.") interp: str | None = None if self.api_key: prompt = ( f"Inversion prep summary:\n" f" Code: {code}, stations: {n_st}, periods: {n_per}\n" f" Period range: {per_range}, error floor: {err_floor}\n" "Give inversion recommendations." ) interp = self.query_llm(prompt, max_tokens=200) elapsed = time.time() - t0 return AgentResult( status="success" if data_file_path else "needs_review", summary=( f"Inversion prep ({code}): {n_st} stations, ~{n_per} periods. " + ( f"Data file: {data_file_path}" if data_file_path else "No data file written." ) ), data={ "data_file_path": data_file_path, "n_periods": n_per, "n_stations": n_st, "code": code, "sites": sites, }, warnings=warnings, llm_interpretation=interp, elapsed_seconds=elapsed, cost_estimate_usd=self._last_cost, )
__all__ = ["InversionPrepAgent"]