Source code for hydromodpy.simulation.planning.plan

"""Immutable runtime objects describing a resolved simulation plan.

This module is the small contract shared by the planner and the runner.
The user-facing TOML remains declarative and compact, while these dataclasses
store the expanded execution schedule the runtime can consume directly.

The main design choice is to keep these objects frozen and explicit:

- one ``ProcessRun`` represents exactly one process/solver pair,
- dependencies are stored as concrete upstream run ids,
- the ``SimulationPlan`` preserves the execution order chosen by the planner.

That makes the schedule easy to inspect in tests, logs, and debugging sessions
without re-reading or re-interpreting the original configuration file.

``RunContext`` and ``RunExecutionResult`` live here so that both the runner
and the adapters share a single import source without circular dependencies.
"""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


[docs] @dataclass(frozen=True) class ProcessRun: """One concrete execution unit produced by the planner. A single declarative ``[[simulation.process]]`` entry can expand to several ``ProcessRun`` objects when multiple solvers are listed. ``depends_on`` stores resolved run identifiers, so the runner can retrieve the exact upstream model instance that must already exist. """ id: str process_id: str process_type: str solver: str backend: str | None = None # Dependencies refer to concrete run ids (for example "flow_1::modflow_nwt"). depends_on: tuple[str, ...] = field(default_factory=tuple)
[docs] @staticmethod def build_id(process_id: str, solver: str) -> str: """Canonical run-id format: ``"<process_id>::<solver>"``.""" return f"{process_id}::{solver}"
@property def is_solver_backed(self) -> bool: """Return True when this run is delegated to a registered solver adapter.""" return self.process_type != "mesh"
[docs] @dataclass(frozen=True) class SimulationPlan: """Resolved execution schedule ready for runtime orchestration. ``runs`` is intentionally ordered: the runner executes it sequentially without re-sorting or topological reconstruction. """ name: str description: str # Preserve the exact execution order emitted by the planner. runs: tuple[ProcessRun, ...] = field(default_factory=tuple)
[docs] def is_empty(self) -> bool: """Return ``True`` when the planner emitted no executable run.""" return not self.runs
def _repr_html_(self) -> str: header = ( "<div><b>SimulationPlan</b> " f"<code>{self.name}</code>" f"<div style='font-size:0.85em;color:#666'>{self.description}</div>" ) if not self.runs: return header + "<i>(empty plan)</i></div>" rows = "".join( "<tr>" f"<td><code>{r.id}</code></td>" f"<td>{r.process_type}</td>" f"<td>{r.solver}</td>" f"<td>{', '.join(r.depends_on) if r.depends_on else '&mdash;'}</td>" "</tr>" for r in self.runs ) table = ( "<table style='font-size:0.85em;border-collapse:collapse'>" "<thead><tr>" "<th style='text-align:left'>run id</th>" "<th style='text-align:left'>type</th>" "<th style='text-align:left'>solver</th>" "<th style='text-align:left'>depends on</th>" "</tr></thead>" f"<tbody>{rows}</tbody></table>" ) return header + table + "</div>"
# --------------------------------------------------------------------------- # Runtime contracts - shared by runner and adapters # ---------------------------------------------------------------------------
[docs] @dataclass(frozen=True) class RunContext: """Resolved runtime context passed to one solver adapter. By the time an adapter receives this object, the planner has already fixed the run order and the runner has already resolved the concrete upstream models referenced by ``run.depends_on``. """ plan: SimulationPlan run: ProcessRun state: Any dependency_models: tuple[Any, ...] = ()
[docs] @dataclass(frozen=True) class RunExecutionResult: """Payload returned by a solver adapter after one run completes. ``primary_model`` is the exact model produced by the run and is always stored under ``state.execution.models_by_run_id[run.id]`` by the runner. ``solver_output_dir`` is the directory where the solver wrote its raw output files (e.g. ``.hds``, ``.cbc``). May be ``None`` for in-memory solvers. ``metrics`` carries lightweight scalar execution metadata produced by the solver itself. It is intentionally optional so older/custom adapters can keep returning only a model and an output directory. """ primary_model: Any solver_output_dir: Path | None = None metrics: Mapping[str, Any] = field(default_factory=dict)