Source code for hydromodpy.core.state.run_state

"""Mutable runtime state threaded through workflow steps.

Three canonical scopes:

- ``setup``: structural objects prepared once (workspace, domain, flow, ...);
- ``loaded_data``: loaded support data (climatic, oceanic, hydrometry, ...);
- ``execution``: run outputs and registries (planned runs, produced models).

Plus result-store lifecycle fields (``store``, ``sim_id``,
``postprocess_runner``) used by the workflow layer.

Canonical access is explicit:

- ``state.setup.<...>`` for structural runtime context,
- ``state.loaded_data.<...>`` for loaded datasets,
- ``state.execution.<...>`` for run outputs and execution registries.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from hydromodpy.core.state.data import LoadedDataContext
from hydromodpy.core.state.execution import ExecutionRegistry
from hydromodpy.core.state.setup import SetupContext


[docs] @dataclass class WorkflowContext: """Mutable workflow state split into setup/loaded_data/execution scopes. ``models_by_run_id`` is the source of truth for produced solver models. Concrete solver instances are resolved explicitly from that registry. Also carries result-store lifecycle fields needed by the workflow layer: ``store``, ``sim_id``, and ``postprocess_runner``. ``cfg`` and ``data_plan`` are typed as ``Any`` because ``core`` cannot import from sibling layers. Concrete types are ``config.hydromodpy_config.HydroModPyConfig`` and ``data.plan.DataLoadPlan``. """ cfg: Any config_path: Path raw_toml: dict[str, Any] data_plan: Any = None setup: SetupContext = field(default_factory=SetupContext) loaded_data: LoadedDataContext = field(default_factory=LoadedDataContext) execution: ExecutionRegistry = field(default_factory=ExecutionRegistry) # Result-store lifecycle (formerly in WorkflowContext only). store: Any = field(default=None, repr=False) sim_id: str | None = None parent_sim_id: str | None = None # Run id minted by the caller before the pipeline starts, consumed once by # the store-opening step. Calibration promotion reserves it so the row # linking the run to its session exists before the run draws its figures. reserved_sim_id: str | None = None postprocess_runner: Any = field(default=None, repr=False) effective_results_config: Any = field(default=None, repr=False) # Dotted results-config paths the planning reconciliation turned on by # itself (never a user choice). Finalization reads them to tell a computed # intermediate apart from a requested output. forced_results_flags: tuple[str, ...] = ()
[docs] def get_model(self, run_id: str) -> Any: """Return the exact model produced by a concrete process run.""" return self.execution.models_by_run_id[run_id]
[docs] def get_run_for_solver(self, solver_name: str) -> Any: """Return the unique planned run matching ``solver_name``, if any.""" matches = [ run for run in self.execution.process_runs_by_id.values() if run.solver == solver_name ] if len(matches) > 1: raise ValueError( f"Expected at most one run for solver '{solver_name}', got {len(matches)}." ) return matches[0] if matches else None
[docs] def get_model_for_solver(self, solver_name: str) -> Any: """Return the produced model for ``solver_name``, if that run completed.""" run = self.get_run_for_solver(solver_name) if run is None: return None return self.execution.models_by_run_id.get(run.id)