Simulation Time Cycle Diagrams#

Scope#

These diagrams document the canonical time cycle driven by [simulation.time] inside the simulation orchestration layer.

They focus on:

  • normalization and validation of the simulation window,

  • canonical stress-period grid construction,

  • strict orchestration-level enforcement for flow-process time grids,

  • typed validation of [recharge_chronicle] payloads before runtime use,

  • propagation toward forcing preparation and flow solvers,

  • generator-based forcing built at fine resolution then aggregated to stress periods,

  • runtime coordination between canonical time_grid and solver tgrid.

The forcing path now separates three responsibilities:

  • hydromodpy.physics.hydrology.synthetic.forcing generates conceptual hydrological signals such as seasonal_step.

  • hydromodpy.physics.forcing.forcing_bridge converts loaded data to homogeneous or heterogeneous solver-ready forcing payloads.

  • hydromodpy.physics.forcing.time_alignment aligns series on simulation stress-period boundaries.

  • hydromodpy.simulation.forcing remains the stable re-export surface used by orchestration code.

Code map#

  • hydromodpy/simulation/settings.py: typed simulation settings and canonical time-window validation.

  • hydromodpy/process/hydrology/synthetic/forcing.py: generator-side synthetic forcing helpers.

  • hydromodpy/process/forcing/forcing_bridge.py: generic forcing conversion and unit handling.

  • hydromodpy/process/forcing/time_alignment.py: stress-period aggregation.

  • hydromodpy/process/flow/time_forcing.py: flow-side forcing preparation before adapter dispatch.

Class Diagram#

@startuml
title Simulation Time Cycle - Class Diagram

package "hydromodpy.simulation.planning.config" {
  class SimulationTimeConfig {
    +mode: "explicit"
    +start_datetime: datetime
    +end_datetime: datetime
    +step_value: int
    +step_unit: hour|day|month|year
    +coverage_policy: error|warn|ignore
  }
}

package "hydromodpy.core.time.window" {
  class ResolvedSimulationTimeWindow {
    +start: Timestamp
    +end: Timestamp
    +step_value: int
    +step_unit: str
    +coverage_policy: str
    +to_date_bounds(): tuple[str, str]
  }

  class ResolvedSimulationTimeGrid {
    +window: ResolvedSimulationTimeWindow
    +boundaries: tuple[Timestamp, ...]
    +period_lengths_seconds: tuple[float, ...]
    +nper: int
    +period_starts: tuple[Timestamp, ...]
    +period_ends_exclusive: tuple[Timestamp, ...]
  }

  class "Time Window Services" as TimeWindowServices <<module>> {
    +resolve_simulation_time_window(cfg)
    +resolve_simulation_time_grid(cfg)
    +require_flow_simulation_time_grid(cfg)
    +apply_explicit_time_window_to_tgrids(cfg)
    +resolve_simulation_time_window_dates(cfg)
    +validate_recharge_coverage(recharge, window)
  }
}

package "hydromodpy" {
  class Project {
    +cfg: HydroModPyConfig
    +time_grid: ResolvedSimulationTimeGrid?
    +run()
    +load_data()
    +_apply_recharge_chronicle_from_toml()
  }
}

package "hydromodpy.physics.forcing.recharge_chronicle_config" {
  class SeasonalStepRechargeChronicleConfig {
    +wet_months: list[int]
    +wet_value: float
    +dry_value: float
  }

  class SyntheticGeneratedRechargeChronicleConfig {
    +values: float|list[float]|None
    +units: str
    +runoff_ratio: float
    +generator: str?
    +generation_step: str
    +seasonal_step: SeasonalStepRechargeChronicleConfig?
  }

  class ObservedRechargeChronicleConfig {
    +path_file: str
    +clim_mod: str
    +clim_sce: str
    +first_year: int?
    +last_year: int?
    +time_step: str?
  }

  class SyntheticCsvRechargeChronicleConfig {
    +path_file: str
    +units: str
    +runoff_units: str?
    +time_step: str?
  }

  class RechargeChronicleConfig {
    +mode: observed_csv|synthetic_generated|synthetic_csv
    +observed_csv: ObservedRechargeChronicleConfig?
    +synthetic_generated: SyntheticGeneratedRechargeChronicleConfig?
    +synthetic_csv: SyntheticCsvRechargeChronicleConfig?
  }

  class "Recharge Chronicle Validation" as RechargeValidation <<module>> {
    +validate_recharge_chronicle_section(value)
  }
}

package "hydromodpy.physics.forcing.recharge_chronicle" {
  class ObservedRechargeChronicleRequest {
    +first_year: int
    +last_year: int
    +time_step: str
  }

  class RechargeChroniclePayload {
    +mode: observed_csv|synthetic_generated|synthetic_csv
    +observed: ObservedRechargeChronicleRequest?
    +recharge: Series?
    +runoff: Series?
  }

  class "Recharge Chronicle Services" as RechargeServices <<module>> {
    +build_recharge_chronicle_payload(raw_toml, simulation_window)
    +align_forcing_series_to_simulation_window(series, simulation_window)
  }
}

package "hydromodpy.physics.hydrology.synthetic.forcing" {
  class "Synthetic Forcing Builders" as HydrologyForcing <<module>> {
    +build_hydrological_step_series(dates, wet_months, wet_value, dry_value)
    +build_recharge_from_reservoir_chronicle(...)
  }
}

package "hydromodpy.data.runtime_loader" {
  class DataManagersRuntimeLoader {
    +_resolve_simulation_time_window_dates(result)
    +_apply_simulation_window_to_station_section(...)
  }
}

package "hydromodpy.solver" {
  class ModflowNwt
  class Modflow6
}

SimulationTimeConfig --> TimeWindowServices : input section
TimeWindowServices ..> ResolvedSimulationTimeWindow : creates
TimeWindowServices ..> ResolvedSimulationTimeGrid : creates

Project --> TimeWindowServices : require_flow_time_grid/apply
Project --> ResolvedSimulationTimeGrid : stores on project state

Project --> RechargeServices : passes simulation_window
RechargeServices --> RechargeValidation : validates raw section
RechargeValidation ..> RechargeChronicleConfig : returns
RechargeServices ..> ResolvedSimulationTimeWindow : aligns forcing series
RechargeServices ..> RechargeChroniclePayload : returns payload
RechargeServices ..> HydrologyForcing : seasonal generator path
RechargeChroniclePayload *-- ObservedRechargeChronicleRequest
RechargeChronicleConfig *-- ObservedRechargeChronicleConfig
RechargeChronicleConfig *-- SyntheticGeneratedRechargeChronicleConfig
RechargeChronicleConfig *-- SyntheticCsvRechargeChronicleConfig
SyntheticGeneratedRechargeChronicleConfig *-- SeasonalStepRechargeChronicleConfig

DataManagersRuntimeLoader --> TimeWindowServices : date bounds lookup

Project --> ModflowNwt : passes required preprocess options.time_grid
Project --> Modflow6 : passes required preprocess options.time_grid
ModflowNwt ..> ResolvedSimulationTimeGrid : consumes nper/perlen
Modflow6 ..> ResolvedSimulationTimeGrid : consumes nper/perlen
@enduml

Structure Diagram#

@startuml
title Simulation Time Cycle - Structure Diagram
skinparam componentStyle rectangle

component "Run TOML\n[simulation.time] + [recharge_chronicle]" as RunToml
component "HydroModPyConfig\n(Pydantic tree)" as TypedConfig
component "Time Core\nhydromodpy.core.time.window" as TimeCore
component "Project Facade\nhydromodpy.Project" as Project
component "Runtime State\nsetup.time_grid" as RuntimeState
component "Solver TGrid Sections\nmodflownwt.tgrid / modflow6.tgrid" as SolverTgrid
component "Data Managers Runtime Loader\nruntime_loader" as DataLoader
component "Recharge Chronicle Config\nforcing.recharge_chronicle_config" as RechargeConfig
component "Recharge Chronicle Builder\nforcing.recharge_chronicle" as RechargeBuilder
component "Hydrology Synthetic Forcing\nhydrology.synthetic.forcing" as HydroForcing
component "Coverage Guard\nvalidate_recharge_coverage()" as CoverageGuard
component "Flow Solver Wrappers\nModflow / Modflow6" as FlowSolvers
database "Climatic Forcing Series\nrecharge / runoff" as ForcingSeries

RunToml --> TypedConfig : load simulation config tree
TypedConfig --> TimeCore : resolve_simulation_time_window/grid

TimeCore --> Project : ResolvedSimulationTimeGrid
Project --> TimeCore : require_flow_simulation_time_grid()
Project --> RuntimeState : persist canonical grid

Project --> TimeCore : apply_explicit_time_window_to_tgrids()
TimeCore --> SolverTgrid : sync nper,lenper,itmuni,genmtd,ntsp,tsmult

Project --> DataLoader : load_all(run_state)
DataLoader --> TimeCore : resolve_simulation_time_window_dates()

Project --> RechargeBuilder : build_recharge_chronicle_payload(..., window)
RunToml --> RechargeConfig : raw [recharge_chronicle] section
RechargeBuilder --> RechargeConfig : validate_recharge_chronicle_section()
RechargeConfig --> RechargeBuilder : normalized mode / units / generator payload
RechargeBuilder --> HydroForcing : build_hydrological_step_series()
HydroForcing --> RechargeBuilder : fine synthetic chronicle
RechargeBuilder --> ForcingSeries : normalize to m/s\nand align to simulation periods
ForcingSeries --> CoverageGuard : recharge coverage check
CoverageGuard --> Project : pass/warn/error

RuntimeState --> FlowSolvers : preprocess options.time_grid
FlowSolvers --> FlowConfig : read flow_regime / first_period_steady
@enduml

Activity Diagram#

@startuml
title Simulation Time Cycle - Activity

start

:Load TOML and build HydroModPyConfig;
:Initialize Project runtime state;

if (Has [simulation.time]?) then (yes)
  :resolve_simulation_time_window();
  if (window invalid?) then (yes)
    :Raise ValueError;
    stop
  endif

  :resolve_simulation_time_grid();
  if (step alignment mismatch?) then (yes)
    :Raise ValueError;
    stop
  endif

  :apply_explicit_time_window_to_tgrids();
  :require_flow_simulation_time_grid();
  :Store time_grid in run_state.setup.time_grid;
else (no)
  if (Flow process declared?) then (yes)
    :Raise ValueError;
    stop
  else (no)
    :Skip canonical time-grid resolution;
  endif
endif

:Load data managers;
if (oceanic/hydrometry/piezometry\nuse_simulation_time_window?) then (yes)
  :resolve_simulation_time_window_dates();
  if (window unresolved?) then (yes)
    :Raise ValueError;
    stop
  else (no)
    :Inject canonical date_start/date_end;
  endif
endif

:Build recharge chronicle payload\nwith simulation_window;
:Validate recharge_chronicle through\nPydantic models;
if (mode = observed_csv?) then (yes)
  :Derive first_year/last_year/time_step\nfrom simulation window;
else (no)
  if (mode = synthetic_generated?) then (yes)
    if (generator configured?) then (yes)
      :Normalize inline units and\nseasonal generator parameters;
      :Build fine synthetic chronicle\nfrom hydrology.synthetic.forcing;
      :Convert recharge/runoff to m/s;
      :Aggregate series to simulation boundaries;
    else (legacy values)
      :Normalize explicit values to target units;
      :Build one value per stress period;
      :Convert recharge/runoff to m/s;
    endif
  else (synthetic_csv)
    :Normalize CSV units;
    :Resample and align series to\nsimulation boundaries;
  endif
endif

:validate_recharge_coverage();
if (coverage_policy = error and invalid?) then (yes)
  :Raise ValueError;
  stop
elseif (coverage_policy = warn and invalid?) then (warn)
  :Emit warning and continue;
endif

while (Next ProcessRun?) is (yes)
  if (Flow solver run?) then (yes)
    if (time_grid available?) then (yes)
      :Use canonical nper/perlen from time_grid;
    else (no)
      :Raise ValueError;
      stop
    endif
    :Execute flow solver;
  else (transport)
    :Resolve upstream flow model;
    :Execute transport solver;
  endif
endwhile (no)

stop
@enduml