Layered Architecture#

HydroModPy is built as a strict layered DAG. Every top-level subpackage of hydromodpy/ belongs to one layer and may only import from the targets declared in tests/unit/architecture/layer_matrix.yaml.

The YAML file is the normative contract, and the tables below are generated from it by tools/doc_contracts.py at every build, so they cannot drift from it. The CI gate reads the same YAML through tests/unit/architecture/test_layer_matrix.py.

Ports and adapters at the storage edge#

The V1 storage contract is DuckDB-first, not backend-agnostic in every runtime path. The project catalog uses CatalogBackend, with DuckDBBackend as the in-tree implementation. The data cache uses a separate DuckDB cache adapter. CLI diagnostics, migration runners and portable-package snapshots may open DuckDB directly when documented as exceptions in Storage Layout.

Field readers go through hmp.read which dispatches to Zarr or Parquet stores via the field registry. See results for the Python surface.

The rules#

  1. One layer per top-level subpackage. Cross-edges that violate the matrix fail CI.

  2. One-way dependencies only. No cycles, even under TYPE_CHECKING.

  3. core is the kernel leaf. It must not import any sibling layer.

  4. Each MODFLOW backend is independent. solver/modflow6/ and solver/modflow_nwt/ never cross-import.

  5. hydromodpy_annex/ may import hydromodpy/. The reverse is forbidden.

  6. Cross-package imports of underscored modules are forbidden. Leading underscore means private to the owning package.

  7. A new edge that violates the matrix is a regression.

Layer matrix#

19 layers, 148 granted edges. A layer always implicitly allows imports targeting itself; anything absent from a row is forbidden and fails CI.

Source layer

Allowed import targets

<root>

<root>, core, schema, config, physics, data, spatial, discretization, simulation, solver, calibration, results, display, analysis, reporting, workflow, catalog, cli

core

core

schema

core, schema, config

config

core, schema, config, physics, data, spatial, simulation, solver, calibration, results, display, analysis, reporting, workflow

physics

core, schema, physics

data

core, schema, data, spatial

spatial

core, schema, spatial

discretization

core, schema, discretization

simulation

core, schema, physics, spatial, data, simulation

solver

core, schema, physics, spatial, discretization, solver, simulation

calibration

core, schema, physics, data, spatial, solver, simulation, calibration, results

results

core, schema, config, results, spatial

display

core, schema, results, display

analysis

core, schema, physics, data, results, display, analysis

reporting

core, schema, config, results, display, analysis, reporting

workflow

core, schema, config, physics, data, spatial, simulation, solver, calibration, results, display, analysis, reporting, workflow

catalog

core, schema, data, results, catalog

project

core, schema, config, physics, data, spatial, simulation, solver, calibration, results, display, analysis, reporting, workflow, catalog, project

cli

<root>, core, schema, config, physics, data, spatial, discretization, simulation, solver, calibration, results, display, analysis, reporting, workflow, catalog, project, cli

Documented tolerances#

7 edges are tolerated rather than granted. Each one carries its reason in the contract. They are temporary or deliberately narrow; tighten one when the edge disappears.

From

To

Why

<root>

project

root facade lazy-resolves Project through hydromodpy.project

analysis

reporting

comparison experiment_launcher writes the HTML report at the end of the orchestration (candidate to move into workflow/)

physics

spatial

FlowConfig embeds spatial FieldSection discriminated union

data

results

cross-DB ATTACH bridge: data.DataEntry.used_by reads results.cross_db (commit 1bd5f31ef)

results

data

cross-DB ATTACH bridge: results.cross_db reads DataCatalogDuckDB via ATTACH read-only (commit 1bd5f31ef)

spatial

data

site-selection BD Topage outlet snapping delegates the optional hydrography fetch to data managers through a narrow helper

calibration

display

network-transient calibration diagnostics reuse the shared static HTML report-block renderer

Files exempt from the layer rule#

5 files are exempt: bootstrap shims that wire forward references, and the case runners that load a user TOML and belong semantically to examples/.

  • hydromodpy/__init__.py

  • hydromodpy/_bootstrap.py

  • hydromodpy/__main__.py

  • hydromodpy/spatial/domain/cases/run_domain_case.py

  • hydromodpy/spatial/geographic/cases/reference_catchment_delineation_case/run_case.py

Special layers#

catalog

Public V1 facade over cache, project catalog and global index. It may import data and results to wrap their stores. The reverse edge is forbidden.

project

Public object-oriented facade. It sits above the matrix like cli so lower layers do not depend on Project.

How CI checks the matrix#

tests/unit/architecture/test_layer_matrix.py parses every Python file in hydromodpy/ and asserts each edge is either allowed or tolerated. It also checks that every declared package has a per-package architecture page.

When refactoring across layers#

If a refactor needs a new edge that the matrix forbids:

  1. Look for an existing intermediary layer.

  2. If none fits, propose the change before touching code.

  3. Update the YAML and the prose together.

  4. Never add a tolerance silently.

See also#