Field Architecture#

This section documents the hydromodpy.spatial.field layer.

Open it when you want:

  • the reusable field abstractions behind FieldSpatial and FieldParam;

  • the concrete mesh implementations reused by geology and solver adapters;

  • the bridge from field-side zoning to solver-side discretization workflows.

Code map#

  • hydromodpy/spatial/field/core/field_spatial.py: reusable field-side zone/value carrier.

  • hydromodpy/spatial/field/core/field_param.py and field_param_config.py: heterogeneous parameter contract consumed downstream by solvers.

  • hydromodpy/spatial/field/meshes/: concrete structured and triangular mesh implementations.

  • hydromodpy/spatial/field/geology/: geology-backed specialisations built on the same field contract.

Core class diagram#

This view is intentionally limited to the reusable field.core abstractions.

@startuml
title Field - Core Class Diagram

package "hydromodpy.spatial.field.core.field_mesh" {
  class MeshCell {
    +index: int
    +kind: str
    +node_indices: tuple[int, ...]
    +vertices: np.ndarray
    +centroid: tuple[float, float]
  }

  class MeshWithValues {
    +mesh: BaseFieldMesh
    +cell_values: np.ndarray
    +label: str | None
    +kind
    +n_cells
  }

  abstract class BaseFieldMesh {
    +kind
    +shape
    +n_nodes
    +n_cells
    +cells
    +iter_cells()
    +cell_centroids()
    +to_cell_values(values)
    +plot_cell_values(ax, cell_values, ...)
    +attach_cell_values(values, label=None)
  }

  abstract class FieldMesh {
    +from_unit_square(target_n_cells, mesh_kind="structured", seed=42)
    +from_dict(config)
    +from_toml(toml_path, section="mesh")
  }
}

package "hydromodpy.spatial.field.core.field_spatial" {
  abstract class Field {
    +identifier: str
    +on_mesh(mesh, cell_samples_per_axis=10)
    +from_dict(config)
    +from_toml(toml_path, section="field")
  }

  abstract class FieldDiscretization {
    +mesh: BaseFieldMesh
    +field_id: str
    +aggregation_name()
    +aggregation
    +weighted_components()
  }
}

package "hydromodpy.spatial.field.core.field_spatial_weighted_discretization" {
  class WeightedAverageFieldDiscretization {
    +zone_keys: tuple[str, ...]
    +fractions_by_zone: dict[str, np.ndarray]
    +aggregation_name()
    +weighted_components()
  }
}

package "hydromodpy.spatial.field.core.field_param" {
  class FieldParam {
    +identifier: str
    +kind: str
    +value: float | None
    +values_by_key: dict[str, float] | None
    +field_spatial_id: str | None
    +to_array(...)
    +to_mesh_field(field_discretization=None, mesh=None, label=None)
    +map_zone_ids(zone_ids)
    +as_dict()
    +from_dict(config)
    +from_toml(toml_path, section="field")
  }
}

BaseFieldMesh "1" *-- "0..*" MeshCell
MeshWithValues --> BaseFieldMesh
BaseFieldMesh --> MeshWithValues : attach_cell_values(...)

FieldMesh ..> BaseFieldMesh : builds one concrete mesh

FieldDiscretization --> BaseFieldMesh
FieldDiscretization <|-- WeightedAverageFieldDiscretization

Field ..> FieldDiscretization : on_mesh(...)

FieldParam ..> BaseFieldMesh : homogeneous mapping
FieldParam ..> FieldDiscretization : heterogeneous mapping

@enduml

Square case relation diagram#

This view shows how the square example specialises the core abstractions and which concrete mesh implementations it reuses.

@startuml
title Field Square Case - Relation To Core

package "hydromodpy.spatial.field.core.field_mesh" {
  abstract class BaseFieldMesh {
    +n_cells
    +cells
    +to_cell_values(values)
    +attach_cell_values(values, label=None)
  }

  abstract class FieldMesh {
    +from_unit_square(target_n_cells, mesh_kind="structured", seed=42)
    +from_dict(config)
  }
}

package "hydromodpy.spatial.field.core.field_spatial" {
  abstract class Field {
    +identifier: str
    +on_mesh(mesh, cell_samples_per_axis=10)
    +from_dict(config)
    +from_toml(toml_path, section)
  }

  abstract class FieldDiscretization {
    +mesh: BaseFieldMesh
    +field_id: str
    +aggregation_name()
    +weighted_components()
  }
}

package "hydromodpy.spatial.field.core.field_spatial_weighted_discretization" {
  class WeightedAverageFieldDiscretization {
    +zone_keys: tuple[str, ...]
    +fractions_by_zone: dict[str, np.ndarray]
    +aggregation_name()
    +weighted_components()
  }
}

package "hydromodpy.spatial.field.core.field_param" {
  class FieldParam {
    +identifier: str
    +kind: str
    +field_spatial_id: str | None
    +to_mesh_field(field_discretization=None, mesh=None, label=None)
  }
}

package "hydromodpy.spatial.field.cases.square.field_spatial_square" {
  class FieldSquare {
    +line: str
    +zone1_side: str
    +zone1_name: str
    +zone2_name: str
    +zone_id(x, y)
    +zone_display(n_plot=320)
    +on_mesh(mesh, cell_samples_per_axis=10)
  }
}

package "hydromodpy.spatial.field.cases.square.field_mesh_square" {
  class FieldMeshSquare {
    +from_unit_square(target_n_cells, mesh_kind, seed)
    +from_dict(config)
  }
}

package "hydromodpy.spatial.field.meshes.structured_field_mesh" {
  class StructuredFieldMesh
}

package "hydromodpy.spatial.field.meshes.triangular_field_mesh" {
  abstract class _TriangularBaseFieldMesh
  class TriangularStructuredFieldMesh
  class TriangularUnstructuredFieldMesh
}

Field <|-- FieldSquare
FieldDiscretization <|-- WeightedAverageFieldDiscretization
FieldDiscretization --> BaseFieldMesh

FieldSquare --> WeightedAverageFieldDiscretization : on_mesh(...)

FieldMesh <|.. FieldMeshSquare
BaseFieldMesh <|-- StructuredFieldMesh
BaseFieldMesh <|-- _TriangularBaseFieldMesh
_TriangularBaseFieldMesh <|-- TriangularStructuredFieldMesh
_TriangularBaseFieldMesh <|-- TriangularUnstructuredFieldMesh

FieldMeshSquare ..> StructuredFieldMesh : builds
FieldMeshSquare ..> TriangularStructuredFieldMesh : builds
FieldMeshSquare ..> TriangularUnstructuredFieldMesh : builds

FieldParam ..> BaseFieldMesh : homogeneous mapping
FieldParam ..> WeightedAverageFieldDiscretization : heterogeneous mapping

@enduml

Activity diagram#

@startuml
title Field - Demo Activity Flow (Square Case)

start

:Parse CLI arguments;
:Resolve config and output paths;
:Load field parameters\nfield_param_from_toml(...);
:Load mesh\nFieldMeshSquare.from_toml(...);

if (field_param.is_heterogeneous?) then (yes)
  :Load spatial field\nFieldSquare.from_toml(...);
  if (field_param.field_spatial_id == field.id?) then (yes)
  else (no)
    :Raise ValueError;
    stop
  endif
  :Discretize field on mesh\nfield.on_mesh(mesh);
  :Compute field values on mesh\nfield_param.to_mesh_field(discretization);
else (no)
  :Broadcast homogeneous value\nfield_param.to_mesh_field(mesh=mesh);
endif

:Create 3-panel figure;
:Save figure in outputs directory;

if (--no-show-plot?) then (yes)
else (no)
  :Display figure with plt.show();
endif

stop
@enduml

Sequence diagram#

@startuml
title Field - Demo Sequence (Square Case)

actor User
participant "run_field_demo.py" as Runner
participant "FieldParam" as Param
participant "FieldMeshSquare" as MeshFactory
participant "FieldSquare" as SpatialField
participant "Matplotlib" as Plot

User -> Runner: execute script
Runner -> Runner: parse args\nresolve paths

Runner -> Param: from_toml(field_param_config)
Param --> Runner: field_param

Runner -> MeshFactory: from_toml(mesh_config)
MeshFactory --> Runner: mesh

alt field_param.is_heterogeneous
  Runner -> SpatialField: from_toml(field_spatial_config)
  SpatialField --> Runner: field
  Runner -> Runner: validate field_spatial_id == field.identifier
  Runner -> SpatialField: on_mesh(mesh)
  SpatialField --> Runner: field_discretization
  Runner -> Param: to_mesh_field(field_discretization)
  Param --> Runner: values_mesh
else homogeneous
  Runner -> Param: to_mesh_field(mesh=mesh)
  Param --> Runner: values_mesh
end

Runner -> Plot: create figure panels
Runner -> Plot: savefig(output_path)
opt interactive mode
  Runner -> Plot: show()
end

Runner --> User: saved figure path
@enduml