Source code for pycsamt.agents.occam2d_agent

"""
pycsamt.agents.occam2d_agent
=============================

:class:`Occam2DAgent` — Write a complete Occam2D inversion input file set.

Produces four files in the output directory:

``OccamDataFile.dat``
    MT response data in Occam2D format.
    Written by :meth:`~pycsamt.models.occam2d.OccamData.write`.

``Occam2DMesh``
    2-D FD mesh derived from the data geometry.
    Written by :meth:`~pycsamt.models.occam2d.mesh.OccamMesh.write`.

``Occam2DModel``
    Mesh-cell → inversion-parameter mapping.
    Written by :meth:`~pycsamt.models.occam2d.model.OccamModel.write`.

``OccamStartup``
    Starting-model and inversion parameters file.
    Written by :meth:`~pycsamt.models.occam2d.startup.OccamStartup.write`.

The agent also validates the written files and returns a brief data summary
so the user can verify the output before running Occam2D.
"""

from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Any

from ._base import AgentResult, BaseAgent

_SYSTEM_PROMPT = """\
You are an expert in 2-D MT inversion setup using Occam2D.
Given the data file statistics and mesh parameters, write 3–4 sentences that:
1. Confirm the data file contains the expected stations and period bands.
2. Comment on the mesh geometry (cell sizes, depth extent, padding).
3. Recommend regularisation parameters (roughness penalty, target RMS).
4. Note any data gaps or stations that should be excluded.
Reply in plain English.
"""


[docs] class Occam2DAgent(BaseAgent): """Generate a complete Occam2D inversion input file set. Parameters ---------- api_key, model, llm_provider : str modes : list of str or None Occam2D mode codes. Common choices: ``["ZXXR","ZXXI","ZXYR","ZXYI","ZYXR","ZYXI","ZYYR","ZYYI"]`` (all Z), ``["RhoZXY","PhsZXY","RhoZYX","PhsZYX"]`` (ρa/φ off-diagonal). Default: ``None`` → OccamData auto-selects from available data. error_floor : float Minimum relative error floor applied to all data (default 0.05 = 5 %). target_rms : float Target RMS for the startup file (default 1.0). Input keys ---------- ``sites`` / ``path`` : Sites or str ``output_dir`` : str ``period_range`` : [T_min, T_max], optional ``modes`` : list of str, optional — overrides constructor default ``title`` : str, optional Output data keys ---------------- ``data_path`` Path — OccamDataFile.dat ``mesh_path`` Path — Occam2DMesh ``model_path`` Path — Occam2DModel ``startup_path`` Path — OccamStartup ``n_stations`` int ``n_periods`` int ``n_data`` int ``output_dir`` str Examples -------- >>> agent = Occam2DAgent() >>> result = agent.execute({ ... "path": "/data/L22PLT", ... "output_dir": "/out/occam2d", ... }) >>> print(result["data_path"]) /out/occam2d/OccamDataFile.dat """ SYSTEM_PROMPT = _SYSTEM_PROMPT def __init__( self, *, api_key: str | None = None, model: str | None = None, llm_provider: str = "claude", modes: list[str] | None = None, error_floor: float = 0.05, target_rms: float = 1.0, ) -> None: super().__init__( "Occam2DAgent", api_key=api_key, model=model, llm_provider=llm_provider, section_preset="inversion", ) self.modes = modes self.error_floor = error_floor self.target_rms = target_rms
[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'.", 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", "pycsamt_occam2d") input_data.get("period_range") modes = input_data.get("modes", self.modes) title = input_data.get( "title", "pycsamt Occam2D — generated by OccamAgent" ) os.makedirs(output_dir, exist_ok=True) # ── import Occam2D model layer ───────────────────────────────────────── try: from ..models.occam2d import OccamData from ..models.occam2d.config import OccamConfig from ..models.occam2d.mesh import OccamMesh from ..models.occam2d.model import OccamModel from ..models.occam2d.startup import OccamStartup except ImportError as exc: return AgentResult.failed( f"pycsamt.models.occam2d not available: {exc}", elapsed=time.time() - t0, ) # ── build OccamData ─────────────────────────────────────────────────── data_path: Path | None = None occ_data: Any = None n_sites = n_periods = n_data = 0 try: cfg = OccamConfig( error_floor_rho=self.error_floor, error_floor_phase=self.error_floor * 10.0, target_misfit=self.target_rms, ) occ_data = OccamData.from_edi( sites, modes=modes, config=cfg, title=title, ) n_sites = int(occ_data.n_sites) n_periods = int(occ_data.n_frequencies) n_data = int(occ_data.n_data) data_path = Path(output_dir) / "OccamDataFile.dat" occ_data.write(data_path) self._log.info( "OccamDataFile.dat written: %d sites × %d freqs", n_sites, n_periods, ) except Exception as exc: return AgentResult.failed( f"OccamData.from_edi failed: {exc}", hint="Check that the EDIs contain valid Z data and coordinates.", elapsed=time.time() - t0, ) # ── build OccamMesh ─────────────────────────────────────────────────── mesh_path: Path | None = None occ_mesh: Any = None try: occ_mesh = OccamMesh.from_data(occ_data) mesh_path = Path(output_dir) / "Occam2DMesh" occ_mesh.write(mesh_path) self._log.info( "Occam2DMesh written: %d × %d cells", getattr(occ_mesh, "n_xcells", "?"), getattr(occ_mesh, "n_zcells", "?"), ) except Exception as exc: warnings.append(f"OccamMesh.from_data failed: {exc}") # ── build OccamModel (parameterisation) ─────────────────────────────── model_path: Path | None = None occ_model: Any = None try: if occ_mesh is not None: occ_model = OccamModel.from_mesh(occ_mesh, config=cfg) model_path = Path(output_dir) / "Occam2DModel" occ_model.write(model_path) self._log.info( "Occam2DModel written: %d params", getattr(occ_model, "n_params", 0), ) except Exception as exc: warnings.append(f"OccamModel.from_mesh failed: {exc}") occ_model = None model_path = None # ── build OccamStartup ──────────────────────────────────────────────── startup_path: Path | None = None try: if occ_model is None: occ_model = OccamModel(description="SMOOTH INVERSION") occ_startup = OccamStartup.from_model(occ_model, config=cfg) startup_path = Path(output_dir) / "OccamStartup" occ_startup.write(startup_path) self._log.info("OccamStartup written") except Exception as exc: warnings.append(f"OccamStartup.from_model failed: {exc}") # ── validate file sizes ─────────────────────────────────────────────── for label, p in [ ("data", data_path), ("mesh", mesh_path), ("model", model_path), ("startup", startup_path), ]: if p and p.exists(): size_kb = p.stat().st_size // 1024 self._log.info("%s file: %s (%d KB)", label, p.name, size_kb) elif p: warnings.append(f"{label} file not found after write: {p}") # ── LLM interpretation ──────────────────────────────────────────────── interp: str | None = None if self.api_key and n_sites: n_x = getattr(occ_mesh, "n_xcells", "?") if occ_mesh else "?" n_z = getattr(occ_mesh, "n_zcells", "?") if occ_mesh else "?" prompt = ( f"Occam2D input file summary:\n" f" Stations: {n_sites}, periods: {n_periods}, data points: {n_data}\n" f" Mesh: {n_x} × {n_z} cells\n" f" Error floor: {self.error_floor:.0%}\n" f" Modes: {modes or 'auto'}\n" f" Warnings: {warnings[:3] if warnings else 'none'}\n\n" "Advise on the inversion setup and recommend regularisation." ) interp = self.query_llm(prompt, max_tokens=200) elapsed = time.time() - t0 files_written = sum( 1 for p in [data_path, mesh_path, model_path, startup_path] if p and p.exists() ) return AgentResult( status="success" if data_path and data_path.exists() else "needs_review", summary=( f"Occam2D prep: {n_sites} stations × {n_periods} periods. " f"{files_written}/4 files written to {output_dir}." ), data={ "data_path": data_path, "mesh_path": mesh_path, "model_path": model_path, "startup_path": startup_path, "n_stations": n_sites, "n_periods": n_periods, "n_data": n_data, "output_dir": output_dir, }, warnings=warnings, llm_interpretation=interp, elapsed_seconds=elapsed, cost_estimate_usd=self._last_cost, )
__all__ = ["Occam2DAgent"]