Source code for hydromodpy.solver.base.adapter_protocol
"""Single ``SolverAdapter`` Protocol.
Every concrete backend (MODFLOW-NWT, MODFLOW 6, Boussinesq, third-party
plugin) implements this same contract so the simulation runner stays
solver-agnostic. Adapters conform *structurally*: there is no base class to
inherit from, just four method signatures plus three ``ClassVar`` attributes
that identify the supported pair and its dependencies.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, ClassVar, Protocol, runtime_checkable
import pandas as pd
from hydromodpy.core.contracts.observables import ObservableRequest, ObservableResult
from hydromodpy.simulation.planning.plan import RunContext, RunExecutionResult
[docs]
@dataclass(frozen=True)
class RunResult:
"""Generic outcome dataclass for plugin authors that need richer payloads
than ``RunExecutionResult``.
Not part of the canonical adapter signature: ``execute`` returns
``RunExecutionResult``. Reserved for diagnostics layers and external
integrations that wrap the simulation runner.
"""
converged: bool
output_dir: Path | None = None
wall_time_s: float | None = None
iterations: int | None = None
residual: float | None = None
diagnostics: dict[str, Any] = field(default_factory=dict)
[docs]
@runtime_checkable
class SolverAdapter(Protocol):
"""One adapter binds one ``(process_type, solver_name)`` pair."""
process_type: ClassVar[str]
solver_name: ClassVar[str]
requires: ClassVar[tuple[tuple[str, str], ...]]
[docs]
def validate(self, ctx: RunContext) -> None:
"""Fail-fast precondition checks before ``execute``."""
[docs]
def execute(self, ctx: RunContext) -> RunExecutionResult:
"""Run the concrete solver for ``ctx.run`` and return its outputs."""
[docs]
def cleanup(self, ctx: RunContext) -> None:
"""Release temporary scratch files after extraction completes."""
@runtime_checkable
class CellLocator(Protocol):
"""A backend that can turn coordinates into one of its own cell selectors.
Separate from :class:`SolverAdapter` because it is a flow-adapter capability,
not a property of every adapter: a transport adapter has no grid of its own
to look on, and folding this into the base protocol would make it fail a
structural check for a method it has no business having.
A gauge, a piezometer and a lake staff are given as coordinates, and only the
backend knows the grid it wrote: structured rows and columns, a Voronoi cell
list, or a flopy model grid. Answering here is what keeps a caller that must
serve every solver from reading the internals of one.
"""
def locate_cell(self, ctx: RunContext, x: float, y: float) -> tuple[int, int, int] | None:
"""Return the cell selector nearest to ``(x, y)``, or ``None``.
The selector is the same triple the observable extractors take:
``(layer, row, col)`` on a structured grid and ``(0, 0, cell_id)`` on an
unstructured one. ``None`` means the run holds no grid to look on yet,
which a caller reports rather than replacing with a guess.
"""
...
__all__ = ["CellLocator", "RunResult", "SolverAdapter"]