Process Architecture#

This page is the code-oriented entry point for hydromodpy.physics, the layer that turns validated configuration into the runtime Flow and Transport objects consumed by solver adapters.

It groups the package map, the reusable contracts, the runtime data model, and every UML diagram (component, class, sequence, lifecycle, extension) in one place.

Package map#

The current hydromodpy.physics stack is split into six concerns:

  • hydromodpy.physics: public compatibility facade that re-exports the main process symbols.

  • hydromodpy.physics.contracts: explicit import path for generic process-layer contracts reused internally.

  • hydromodpy.physics.prototype: process-agnostic building blocks (ProcessSpatial, ProcessSpatialConfig, InitialCondition, BoundaryCondition, SinkSource).

  • hydromodpy.physics.flow: concrete flow process plus typed config and payload models.

  • hydromodpy.physics.transport: concrete transport process plus typed config.

  • hydromodpy.physics.forcing: helpers that turn loaded data into process-ready forcing payloads aligned to simulation time.

  • hydromodpy.physics.hydrology: hydrological utilities, synthetic forcing helpers, and the pyhelp coupling stack.

Runtime role:

  • physics defines the hydrological problem payloads,

  • simulation decides when those payloads are executed,

  • solver decides how they are numerically solved.

Reading paths by concern#

Generic process contracts (“what is the shared contract behind process objects?”):

  1. hydromodpy/process/contracts.py

  2. hydromodpy/process/prototype/__init__.py

  3. files under hydromodpy/process/prototype/

Flow process (“what does the project materialize before a flow solve?”):

  1. hydromodpy/process/flow/__init__.py

  2. hydromodpy/process/flow/flow.py

  3. hydromodpy/process/flow/flow_config.py

  4. initial / boundary / sinks-source payload files in the same package

Transport process (“what transport runtime object is passed to the adapter?”):

  1. hydromodpy/process/transport/transport.py

  2. hydromodpy/process/transport/transport_config.py

Forcing bridge (“how do loaded data become solver-ready time series?”):

  1. hydromodpy/process/forcing/forcing_bridge.py

  2. hydromodpy/process/forcing/time_alignment.py

Adding a new process (extension workflow): see the activity diagram below.

Layer separation (component diagram)#

Architectural boundaries between configuration, runtime process objects, conceptual hydrology helpers, adapter logic, and solver backends.

@startuml
title Process - Layer Separation Component Diagram
left to right direction

package "User / Orchestration Layer" {
  component "Workflow Scripts" as WorkflowScripts
  component "HydroModPy API Calls" as ApiCalls
}

package "Configuration Layer" {
  component "HydroModPyConfig" as HydroModPyConfig
  component "FlowConfig" as FlowConfig
  component "TransportConfig" as TransportConfig
  component "Section Normalizers\n(param / ic / bc / sinks_sources)" as Normalizers
}

package "Conceptual Hydrology Layer" {
  component "Recharge Chronicle Config\nsimulation.forcing.recharge_chronicle_config" as RechargeChronicleConfig
  component "Recharge Chronicle Adapter\nsimulation.forcing.recharge_chronicle" as RechargeChronicleAdapter
  component "Synthetic Forcing\nhydrology.synthetic.forcing" as HydrologySyntheticForcing
}

package "Runtime Process Layer" {
  component "ProcessSpatial" as ProcessSpatial
  component "Flow" as Flow
  component "Transport" as Transport
  component "Typed Runtime Payloads" as RuntimePayloads
}

package "Adapter Layer" {
  component "flow_to_modflow_adapter (NWT)" as FlowAdapterNwt
  component "MF6 Assembly Helpers" as Mf6Helpers
}

package "Solver Backend Layer" {
  component "MODFLOW-NWT Wrapper" as ModflowNwtWrapper
  component "MODFLOW 6 Wrapper" as Modflow6Wrapper
}

WorkflowScripts --> HydroModPyConfig
ApiCalls --> HydroModPyConfig

HydroModPyConfig --> FlowConfig
HydroModPyConfig --> TransportConfig
HydroModPyConfig --> RechargeChronicleConfig
FlowConfig --> Normalizers
TransportConfig --> Normalizers
RechargeChronicleConfig --> RechargeChronicleAdapter
RechargeChronicleAdapter --> HydrologySyntheticForcing

FlowConfig --> Flow
TransportConfig --> Transport
Flow --> ProcessSpatial : extends
Transport --> ProcessSpatial : extends
Flow --> RuntimePayloads
Transport --> RuntimePayloads

Flow --> FlowAdapterNwt
Flow --> Mf6Helpers
RuntimePayloads --> FlowAdapterNwt
RuntimePayloads --> Mf6Helpers
RechargeChronicleAdapter --> FlowAdapterNwt
RechargeChronicleAdapter --> Mf6Helpers

FlowAdapterNwt --> ModflowNwtWrapper
Mf6Helpers --> Modflow6Wrapper
@enduml

Notes:

  • Config parsing and validation are isolated from solver-specific code.

  • Conceptual hydrology forcing remains outside hydromodpy.physics and is exposed through the simulation forcing adapter layer.

  • Runtime process classes are solver-agnostic containers.

  • Adapter components are the only layer allowed to translate runtime data to solver input formats.

Config class diagram#

Validated configuration classes (Pydantic models): shared ProcessSpatialConfig base, FlowConfig and TransportConfig specialisations, flow-specific initial conditions, boundary conditions, and sink/source configs.

@startuml
title Process - Config Class Diagram

package "hydromodpy.physics.base" {
  class ProcessSpatialConfig {
    +param_list: list[str]
    +param: dict[str, object]
    +ic: object | None
    +bc: dict[str, object]
    +sinks_sources: dict[str, object]
  }

  class InitialCondition
}

package "hydromodpy.physics.flow" {
  class FlowConfig {
    +flow_regime: steady | transient
    +param_list: list[str]
    +param: dict[str, dict]
    +bc: dict[str, object]
    +ic: FlowInitialConditions | None
    +sinks_sources: FlowSinksSourcesConfig
    +from_toml_section(flow_section, base_dir)
  }

  class FlowInitialCondition {
    +type: top | bottom | custom
    +value: float | None
  }
  class FlowInitialConditions {
    +h: FlowInitialCondition
  }
  class FlowBoundaryConditionConfig {
    +id: str
    +value: float
    +description: str
    +units: str
    +type: dirichlet | cauchy | robin
    +data_value: bool
    +application_domain: str | None
  }
  class FlowWellConfig {
    +cell: tuple[int, int, int]
    +flux: float | list[float]
    +units: str
    +description: str
  }
  class FlowSinksSourcesConfig {
    +wells: dict[str, FlowWellConfig]
  }
}

package "hydromodpy.physics.transport" {
  class TransportConfig {
    +particle: TransportParticleConfig
    +conc: TransportConcConfig
    +param_list: list[str] (excluded)
    +param: dict[str, object] (excluded)
    +ic: object | None (excluded)
    +bc: dict[str, object] (excluded)
    +sinks_sources: dict[str, object] (excluded)
  }

  class ParticleParametersConfig
  class TransportParticleConfig {
    +parameters: ParticleParametersConfig
  }
  class ConcParametersConfig
  class TransportConcConfig {
    +parameters: ConcParametersConfig
  }
}

ProcessSpatialConfig <|-- FlowConfig
ProcessSpatialConfig <|-- TransportConfig

InitialCondition <|-- FlowInitialCondition
FlowInitialConditions *-- FlowInitialCondition : h
FlowConfig --> FlowInitialConditions : ic
FlowConfig --> FlowBoundaryConditionConfig : bc values
FlowConfig --> FlowSinksSourcesConfig : sinks_sources
FlowSinksSourcesConfig *-- FlowWellConfig : wells

TransportConfig *-- TransportParticleConfig : particle
TransportConfig *-- TransportConcConfig : conc
TransportParticleConfig *-- ParticleParametersConfig : parameters
TransportConcConfig *-- ConcParametersConfig : parameters
@enduml

Notes:

  • FlowConfig and TransportConfig inherit from ProcessSpatialConfig.

  • FlowInitialCondition inherits from prototype InitialCondition.

  • FlowBoundaryConditionConfig and FlowSinksSourcesConfig are dedicated flow models (not subclasses of prototype BoundaryCondition / SinkSource).

  • TransportConfig currently keeps boundary and sink/source payloads as generic mappings.

Runtime class diagram#

Runtime inheritance and composition for process objects: ProcessSpatial as the abstract runtime base, Flow and Transport as concrete implementations, runtime initial conditions, boundary conditions, and sink/source containers.

@startuml
title Process - Runtime Class Diagram

package "hydromodpy.physics.base" {
  abstract class "ProcessSpatial<TInitialConditions>" as ProcessSpatialT {
    +parameters: dict[str, object]
    +initial_conditions: TInitialConditions | None
    +boundary_conditions: dict[str, BoundaryCondition]
    +sinks_sources: dict[str, object]
    +set_parameters_from_config(parameters, parameter_ids, context_label)
    +build_initial_conditions(initial_conditions)
    +set_boundary_conditions(boundary_conditions)
    +set_sinks_sources(sinks_sources)
  }

  class InitialCondition
  class BoundaryCondition
  class SinkSource
}

package "hydromodpy.physics.flow" {
  class Flow {
    +config: FlowConfig
    +flow_regime: str
    +initial_conditions: FlowInitialConditions | None
    +boundary_condition_application_domains: dict[str, str]
    +set_config(config)
    +set_initial_conditions(initial_conditions)
    +set_boundary_conditions(boundary_conditions, application_domains)
    +set_sinks_sources(sinks_sources)
  }

  class FlowConfig
  class FlowInitialCondition {
    +type: top | bottom | custom
    +value: float | None
  }
  class FlowInitialConditions {
    +h: FlowInitialCondition
  }
  class FlowSinksSourcesConfig
}

package "hydromodpy.physics.transport" {
  class Transport {
    +config: TransportConfig | None
    +particle: _TransportComponent
    +conc: _TransportComponent
    +set_config(config)
    +set_parameters(parameters)
    +build_initial_conditions(initial_conditions)
    +set_boundary_conditions(boundary_conditions)
    +set_sinks_sources(sinks_sources)
  }

  class TransportConfig
  class TransportInitialConditions {
    +payload: dict[str, Any]
  }
  class _TransportComponent {
    +parameters: dict[str, Any]
    +set_parameters(parameters, kwargs)
  }
}

ProcessSpatialT <|-- Flow
ProcessSpatialT <|-- Transport

Flow --> FlowConfig : consumes
Transport --> TransportConfig : consumes

InitialCondition <|-- FlowInitialCondition
FlowInitialConditions *-- FlowInitialCondition : h
Flow --> FlowInitialConditions : initial_conditions
Transport --> TransportInitialConditions : initial_conditions

ProcessSpatialT --> BoundaryCondition : boundary_conditions values
ProcessSpatialT ..> SinkSource : add_sink_source()

Transport *-- _TransportComponent : particle
Transport *-- _TransportComponent : conc
Flow --> FlowSinksSourcesConfig : set_sinks_sources input
@enduml

Notes:

  • Flow and Transport both inherit from ProcessSpatial.

  • FlowInitialCondition inherits from prototype InitialCondition.

  • Runtime boundary conditions stored by ProcessSpatial use prototype BoundaryCondition.

  • Runtime sink/source storage is generic (dict[str, object]), with process-specific payloads injected by child classes.

  • Recharge chronicle preparation stays outside this inheritance tree and is handled by simulation forcing services before solver assembly.

Lifecycle state machine#

The usual lifecycle of a ProcessSpatial-based runtime object, from creation to solver execution and post-processing.

@startuml
title ProcessSpatial - Lifecycle State Machine

[*] --> Created

Created --> ConfigValidated : validate config model\n(FlowConfig / TransportConfig)
ConfigValidated --> RuntimeHydrated : apply set_config()\nparameters + ic + bc + sinks_sources
RuntimeHydrated --> PreparedForSolver : build adapter payloads
PreparedForSolver --> Solving : launch solver run

Solving --> Solved : solver success
Solving --> Failed : solver or IO exception

Solved --> PostProcessed : export diagnostics\nand compute derived outputs
PostProcessed --> RuntimeHydrated : update parameters/reload config
Failed --> RuntimeHydrated : fix input and retry

PostProcessed --> [*]
@enduml

Notes:

  • RuntimeHydrated means parameters, IC, BC, and sinks/sources are set.

  • PreparedForSolver represents adapter-resolved arrays / dictionaries.

  • Failures can route back to hydration after config or data corrections.

Runtime-to-solver sequence#

The main runtime handoff from process objects to solver backends: runtime construction of Flow from validated config, recharge chronicle preparation before solver assembly, adapter-level transformation into solver payloads, and backend-specific execution.

@startuml
title Process - Runtime To Solver Sequence

actor User
participant "Workflow Runner\n(example script / API)" as Runner
participant "FlowConfig" as FlowConfig
participant "Flow" as Flow
participant "Recharge Chronicle Builder" as RechargeBuilder
participant "FlowAdapter (NWT)" as FlowAdapter
participant "MODFLOW-NWT Wrapper" as NWT
participant "MODFLOW 6 Wrapper" as MF6

User -> Runner: launch simulation workflow
Runner -> FlowConfig: from_toml_section(flow_section, base_dir)
FlowConfig --> Runner: validated FlowConfig

Runner -> Flow: Flow(config)
Flow -> Flow: set_config(...)\nset_parameters_from_config(...)\nset_initial_conditions(...)\nset_boundary_conditions(...)\nset_sinks_sources(...)
Flow --> Runner: runtime process object

Runner -> RechargeBuilder: build_recharge_chronicle_payload(raw_toml, window)
RechargeBuilder -> RechargeBuilder: validate config,\nnormalize units,\nalign forcing to stress periods
RechargeBuilder --> Runner: recharge + runoff series

Runner -> Runner: select solver backend

alt backend = nwt
  Runner -> FlowAdapter: build solver-ready payloads\nfrom Flow + domain + canonical time_grid + recharge
  FlowAdapter -> FlowAdapter: normalize heads, BC, wells,\nrecharge and application domains
  FlowAdapter --> Runner: adapter payloads
  Runner -> NWT: configure model packages
  Runner -> NWT: run()
  NWT --> Runner: heads + flows outputs
else backend = mf6
  Runner -> MF6: configure(flow, domain, canonical time_grid, recharge)
  MF6 -> MF6: map runtime BC/IC/sinks\nand recharge into MF6 package data
  Runner -> MF6: run()
  MF6 --> Runner: heads + budget outputs
end

Runner --> User: simulation results and artifacts
@enduml

Notes:

  • The sequence is logical and backend-agnostic at the high level.

  • Payload conversion is explicitly separated from process runtime state.

  • Recharge forcing is prepared before solver assembly and injected as already-aligned series.

  • Solver wrappers remain consumers of already-normalised process data.

  • For detailed DIS payload semantics, see MODFLOW-NWT Contracts.

Extending with a new process#

Practical workflow to add a new ProcessSpatial specialisation: config model first, then runtime class, then adapters. Testing and documentation updates are mandatory completion steps; iteration is expected before final integration.

Reading path before extending:

  1. hydromodpy/process/prototype/process_spatial_config.py

  2. hydromodpy/process/prototype/process_spatial.py

  3. hydromodpy/process/flow/ as the main concrete example

  4. hydromodpy/solver/base/registry.py

  5. hydromodpy/solver/compatibility.py

@startuml
title Process - Extension Activity (Add New ProcessSpatial Specialization)

start

:Define process scope and runtime variables;
:Create new config model\nnew_process_config.py;
:Implement section normalizers\n(param / ic / bc / sinks_sources);
:Implement runtime class\nclass NewProcess(ProcessSpatial);
:Wire package exports in __init__.py;
:Implement solver adapter translation layer;
:Integrate with solver entry points;
:Add unit tests (config + runtime + adapter);
:Add or update example workflow;
:Add architecture/doc pages and UML sources;
:Run tests and checks;

if (All tests and checks pass?) then (yes)
  :Open PR / merge changes;
  stop
else (no)
  :Fix validation, mapping,\nor runtime issues;
  :Re-run tests and checks;
  :Repeat fix/check cycle until green;
  :Open PR / merge changes;
  stop
endif

@enduml

See also#