Boussinesq Solver UML Diagrams#

Scope#

These diagrams document the software architecture of hydromodpy.solver.boussinesq.

They are intentionally limited to four views:

  • package context;

  • core classes;

  • process-to-backend runtime sequence;

  • transient-step activity.

This is the right level for this package. The mathematical derivation already lives in the scientific notes, so the architecture documentation should focus on structure, responsibilities and handoff boundaries.

Simplification Review#

The current Boussinesq package is already much clearer than before:

  • the canonical Dirichlet concept is now prescribed boundary cells;

  • the active driver/runtime path now uses that prescribed-cell representation;

  • edge-based boundary diagnostics are rebuilt explicitly in boundary_flux_reconstruction.py instead of driving the solve path;

  • the method and engine taxonomy is explicit;

  • runtime state construction is centralized;

  • steady and transient orchestration have been extracted out of the main driver;

  • process-to-runtime normalization is extracted into solver_contract.py;

  • forcing resolution is now split into a stable facade plus specialized submodules;

  • shared boundary/ocean/drainage preparation is isolated in drivers/forcing.py;

  • the public assembly layer is now one facade over dedicated internal modules for inputs, fluxes, surface closures and residual builders;

  • the public semianalytic Jacobian layer is now one facade over dedicated internal modules for common helpers and sparse triplet builders;

  • runtime summary shaping and common runtime-result packaging are extracted into dedicated helpers;

  • the process-to-solver contract is explicit.

The main remaining simplification targets are:

  • keep the driver thin and avoid regrowing orchestration logic in boussinesq.py;

  • keep future cleanups focused on assembly_residuals.py, assembly_fluxes.py, jacobian_operator_triplets.py and jacobian_partition_triplets.py, which are now the main size hotspots;

  • continue factoring small pieces of common runtime bookkeeping only when the resulting helper stays clearer than the duplicated code.

Diagram 1: Package Context#

@startuml
allowmixing
title Boussinesq Solver - Package Context

package "process" {
  class Flow
}

package "hydromodpy.solver.boussinesq" {
  class Boussinesq
  class BoussinesqMesh
  class BoussinesqState
  class "assembly.py" as AssemblyModule <<module>>
  class "runtime_selection.py" as RuntimeSelection <<module>>
  class "runtime_contract.py" as RuntimeContract <<module>>
  package methods
  package engines
  package formulations
  package discretization
}

package "runtime engines" {
  class "local_runtime.py" as LocalRuntime <<module>>
  class "scipy_runtime.py" as ScipyRuntime <<module>>
  class "scipy_sparse_runtime.py" as SparseRuntime <<module>>
  class "petsc_partition_runtime.py" as PetscPartitionRuntime <<module>>
  class "petsc_runtime.py" as PetscRuntime <<module>>
}

Flow --> Boussinesq : hydrological definition
Boussinesq --> BoussinesqMesh : builds and uses
Boussinesq --> RuntimeSelection : resolves method + engine
RuntimeSelection --> methods
RuntimeSelection --> engines
methods --> formulations
methods --> discretization
Boussinesq --> RuntimeContract : packages solve inputs
LocalRuntime ..> AssemblyModule : assembles residual
ScipyRuntime ..> AssemblyModule : assembles residual
SparseRuntime ..> AssemblyModule : assembles residual
PetscPartitionRuntime ..> AssemblyModule : assembles residual
PetscRuntime ..> AssemblyModule : assembles residual
Boussinesq --> BoussinesqState : stores accepted state

note right of Boussinesq
Main orchestrator.
Still the largest file in the package.
Next simplification target:
split payload resolution,
transient orchestration
and export assembly.
end note

note bottom of AssemblyModule
Physical core.
Should keep only one canonical
boundary language internally:
prescribed boundary cells.
end note

@enduml

Diagram 2: Core Classes#

@startuml
allowmixing
title Boussinesq Solver - Core Classes

package "hydromodpy.solver.boussinesq" {
  class Boussinesq {
    +pre_processing()
    +processing()
    +post_processing()
    +_resolve_solver_contract()
    +_run_steady_runtime()
    +_run_transient_runtime()
  }

  class BoussinesqSolverContract {
    +flow_regime: str
    +runtime_backend_requested: str
    +surface_interaction_model_requested: str
    +surface_interaction_model_resolved: str
    +runtime_backend: BoussinesqRuntimeBackend
  }

  class BoussinesqMesh
  class BoussinesqState {
    +initial(...)
    +from_runtime(...)
  }

  class BoussinesqAssembly {
    +head_m
    +saturated_thickness_m
    +transmissivity_m2_s
    +internal_edge_flux_m3_s
    +prescribed_head_flux_m3_s
    +drainage_flux_m3_s
    +residual_m3_s
  }

  class TransientStepInputs
  class SteadySolveInputs
  class RuntimeSolveResult
  class BoussinesqRuntimeBackend
  class BoussinesqMethodSpec
  class BoussinesqEngineSpec
}

Boussinesq --> BoussinesqMesh : uses
Boussinesq --> BoussinesqState : creates/stores
Boussinesq --> BoussinesqSolverContract : resolves
BoussinesqSolverContract --> BoussinesqRuntimeBackend
BoussinesqRuntimeBackend --> BoussinesqMethodSpec
BoussinesqRuntimeBackend --> BoussinesqEngineSpec
BoussinesqRuntimeBackend --> TransientStepInputs : consumes
BoussinesqRuntimeBackend --> SteadySolveInputs : consumes
BoussinesqRuntimeBackend --> RuntimeSolveResult : returns
RuntimeSolveResult --> BoussinesqAssembly

note bottom of BoussinesqState
Canonical accepted state for
diagnostics and exports.
Legacy imposed_head_* fields
still exist here for compatibility.
end note

note right of BoussinesqSolverContract
Important clarification layer:
problem definition
is separated from
method and engine resolution.
end note

@enduml

Diagram 3: Process To Backend Sequence#

@startuml
title Boussinesq Solver - Process To Backend Sequence

actor Project
participant Flow
participant "Boussinesq" as Solver
participant "BoussinesqSolverContract" as Contract
participant "runtime_selection" as Selection
participant "Resolved Backend" as Backend
participant "assembly.py" as Assembly
participant "BoussinesqState" as State

Project -> Solver : processing(run_model=True)
Solver -> Solver : _build_initial_state()
Solver -> Contract : _resolve_solver_contract(flow)
Contract -> Selection : resolve_runtime_backend(...)
Selection --> Contract : BoussinesqRuntimeBackend
Contract --> Solver : resolved contract

alt steady
  Solver -> Backend : solve_steady_problem(SteadySolveInputs)
else transient
  loop each stress period
    Solver -> Backend : solve_transient_step(TransientStepInputs)
    Backend -> Assembly : assemble residual/Jacobian
    Assembly --> Backend : BoussinesqAssembly
    Backend --> Solver : RuntimeSolveResult
  end
end

Solver -> State : from_runtime(...)
Solver -> Solver : post_processing()

note over Flow, Solver
Flow stays solver-agnostic.
The Boussinesq driver translates
that process definition into
cell-wise arrays and one explicit
method and engine contract.
end note

note over Backend, Assembly
All runtimes consume the same
physical inputs and return the same
RuntimeSolveResult shape.
end note

@enduml

Diagram 4: Transient Step Activity#

@startuml
title Boussinesq Solver - Transient Step Activity

start
:Read previous accepted head;
:Resolve recharge, wells,\nboundary payloads for kper;
:Resolve solver contract\n(method + engine);
:Build TransientStepInputs;

if (Prescribed boundary cells?) then (yes)
  :Overwrite prescribed cells\nin canonical head view;
else (no)
  :Keep free-cell state;
endif

:Assemble spatial terms;
:Add transient storage term;
:Run nonlinear backend\n(local / SciPy / PETSc);

if (Converged?) then (yes)
  :Accept RuntimeSolveResult;
  :Update BoussinesqState;
  :Append histories and summaries;
else (no)
  :Store failure diagnostics;
endif

:Build export payload;

if (Legacy consumers still need\nimposed_head_* ?) then (yes)
  :Emit compatibility view;
else (no)
  :Export canonical prescribed_* only;
endif

stop
@enduml

Notes#

  • The canonical runtime path is now based on prescribed_head_m_by_cell.

  • The main orchestration split is now:

    • boussinesq.py for top-level coordination;

    • drivers/steady.py and drivers/transient.py for solve execution;

    • solver_contract.py for process-to-runtime normalization;

    • runtime_summary.py for runtime-summary shaping;

    • forcing_resolution.py plus forcing/ for process-to-array mapping;

    • drivers/forcing.py for shared boundary and drainage preparation;

    • jacobian/semianalytic.py plus its internal triplet modules for the semianalytic linearization layer.

  • The diagrams deliberately separate:

    • hydrological problem definition;

    • method and formulation selection;

    • execution-engine selection;

    • nonlinear runtime execution;

    • export and diagnostics.