Structured Grid Architecture#

This page documents the FloPy StructuredGrid path used by the MODFLOW-family solvers: static object model, build sequence, and the bridge to Field / FieldParam for solver-ready arrays.

Code map#

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_config.py: validated grid config contract.

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_from_config.py: public build entry point.

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_generation.py: StructuredGridBuilder and geometric assembly.

  • hydromodpy/spatial/mesh/cartesian_grid/utils/raster_grid_reader.py: top-surface raster loading.

  • hydromodpy/spatial/mesh/cartesian_grid/utils/planar_discretizer.py: planar discretization helpers.

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_mesh_adapter.py: geometry bridge from solver grid to field-side mesh logic.

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_field_discretization.py: field discretization helpers.

  • hydromodpy/spatial/mesh/cartesian_grid/sgrid_fieldparam_discretization.py: heterogeneous parameter mapping to solver arrays.

  • hydromodpy/solver/modflow_common/solver_mesh.py: solver-side consumer of the resolved mesh contract.

  • hydromodpy/spatial/field/core/field_param.py: upstream field parameter contract.

Recommended reading path:

  1. sgrid_config.py and sgrid_from_config.py

  2. raster_grid_reader.py and planar_discretizer.py

  3. sgrid_generation.py

  4. sgrid_mesh_adapter.py

  5. sgrid_field_discretization.py and sgrid_fieldparam_discretization.py

Static class diagram#

The static object model around FloPy StructuredGrid: HydroModPy domain objects that feed the builder, the narrow responsibility of StructuredGridBuilder, the boundary between HydroModPy classes and the external FloPy grid, and the downstream adapters that read StructuredGrid geometry.

Reading guide:

  • --> means a stable relation or a produced object.

  • ..> means a transient usage or read dependency.

@startuml
title StructuredGrid - Core Class Diagram
left to right direction
hide empty members
hide circle
skinparam linetype ortho
skinparam packageStyle rectangle

legend right
|= Arrow |= Meaning |
| ``-->`` | stable relation or produced object |
| ``..>`` | transient usage or read dependency |
endlegend

package "HydroModPy domain" {
  class RasterSupport <<domain>> {
    +crs
    +dx / dy
    +xmin / xmax
    +ymin / ymax
    +nrows / ncols
    +nodata
  }

  class Surface <<domain>> {
    +name
    +values
    +support
    +as_array()
    +resample_to_shape(...)
  }
}

package "Grid build" {
  class VerticalGridConfig <<config>> {
    +genmtd_lay
    +nlay
    +lay_decay
    +lay_proportions
    +nodata
  }

  class StructuredGridBuilder <<builder>> {
    +build_from_surfaces(top_surface, bottom_surface, vertical_config)
  }
}

package "FloPy" {
  class StructuredGrid <<external>> {
    +nlay / nrow / ncol
    +delr / delc
    +top / botm
    +xoff / yoff
    +xvertices / yvertices
  }
}

package "Downstream adapters" {
  class SGridMeshAdapter <<adapter>> {
    +extract_structured_vertices(sgrid)
    +build_field_mesh_from_sgrid(sgrid)
  }

  class StructuredFieldMesh <<field mesh>>

  class SGridFieldParamDiscretizer <<adapter>> {
    +discretize_fieldparam_on_sgrid(..., sgrid, ...)
  }
}

note top of StructuredGridBuilder
Single responsibility:
- validate top/bottom compatibility
- compute vertical layering
- instantiate FloPy StructuredGrid
end note

Surface --> RasterSupport : carries support metadata

StructuredGridBuilder ..> Surface : reads top_surface\nand bottom_surface
StructuredGridBuilder ..> VerticalGridConfig : reads layering rules
StructuredGridBuilder --> StructuredGrid : builds

SGridMeshAdapter ..> StructuredGrid : reads XY geometry
SGridMeshAdapter --> StructuredFieldMesh : builds planar mesh

SGridFieldParamDiscretizer ..> StructuredGrid : reads top/botm\nand grid dimensions

@enduml

Notes:

  • StructuredGrid itself belongs to FloPy. The diagram focuses on the HydroModPy classes that prepare or consume it.

  • SGridConfig, RasterGridReader, and PlanarDiscretizer are not shown here on purpose; they belong to the build sequence below.

Build sequence#

The dynamic workflow that turns one user-facing SGrid config payload into one FloPy StructuredGrid: validation and normalisation of SGridConfig, top-surface loading and planar discretisation, the branches that resolve the bottom surface, and the final handoff to StructuredGridBuilder.

Reading guide:

  • -> means a call.

  • --> means a returned value.

  • alt blocks show the mutually exclusive bottom-surface strategies driven by cfg.genmtd_bot.

@startuml
title StructuredGrid - Build Sequence
autonumber
hide footbox
skinparam sequenceMessageAlign center
skinparam responseMessageBelowArrow true

legend right
|= Arrow |= Meaning |
| ``->`` | call |
| ``-->`` | returned value |
| ``alt`` | one branch chosen from config |
endlegend

actor Caller
participant "build_sgrid_from_config(...)\n<<module function>>" as Factory
participant "SGridConfig\n<<config>>" as Cfg
participant "RasterGridReader" as Reader
participant "PlanarDiscretizer" as Discretizer
participant "top_surface : Surface" as TopSurface
participant "bottom_surface : Surface" as BottomSurface
participant "VerticalGridConfig\n<<config>>" as VerticalCfg
participant "StructuredGridBuilder\n<<builder>>" as Builder
participant "StructuredGrid\n<<external FloPy>>" as Grid

Caller -> Factory: build_sgrid_from_config(config)
Factory -> Cfg: _coerce_sgrid_config(config)
Cfg --> Factory: validated cfg

Factory -> Reader: read_top_grid(cfg.top_path)
Reader --> Factory: source_top_grid

Factory -> Discretizer: discretize_top(source_top_grid,\nplan mode, nx, ny, nodata, crs)
Discretizer --> Factory: target_top_grid

Factory -> TopSurface: _surface_from_top_grid(target_top_grid)
TopSurface --> Factory: top_surface

alt cfg.genmtd_bot == "constant_thickness"
  Factory -> BottomSurface: Surface(top_surface - thick)
  BottomSurface --> Factory: bottom_surface
else cfg.genmtd_bot == "constant_altitude"
  Factory -> BottomSurface: Surface(flat zbot)
  BottomSurface --> Factory: bottom_surface
else cfg.genmtd_bot == "filepath"
  Factory -> Reader: read_band1_with_metadata(cfg.bot_path)
  Reader --> Factory: bottom band + metadata
  Factory -> BottomSurface: _surface_from_band_metadata(...)
  BottomSurface --> Factory: source_bottom_surface
  opt cfg.plan_discretization_mode == "resample_to_shape"
    Factory -> BottomSurface: resample_to_shape(target top shape)
    BottomSurface --> Factory: resampled bottom_surface
  end
else cfg.genmtd_bot == "raster"
  Factory -> BottomSurface: Surface(bot_raster or resampled bot_raster)
  BottomSurface --> Factory: bottom_surface
end

Factory -> VerticalCfg: derive from cfg
VerticalCfg --> Factory: vertical_config

Factory -> Builder: build_from_surfaces(top_surface,\nbottom_surface, vertical_config)
activate Builder
Builder -> Builder: validate shared geographic domain
Builder -> Builder: compute layer proportions
Builder -> Builder: build botm, delr, delc
Builder -> Grid: StructuredGrid(...)
Grid --> Builder: solver grid
Builder --> Factory: StructuredGrid
deactivate Builder

Factory --> Caller: StructuredGrid
@enduml

Notes:

  • build_sgrid_from_config(...) owns config resolution and surface preparation.

  • StructuredGridBuilder stays narrower: it validates top/bottom geometric consistency, computes vertical layering, and instantiates the FloPy grid.

SGrid / FieldParam discretization#

How HydroModPy bridges FloPy StructuredGrid (solver side), planar field meshes (field/geology side), and Field / FieldParam value mapping for solver-ready arrays.

Class view:

@startuml
title Spatial Discretization - Class Diagram (SGrid, Mesh, FieldParam)

package "flopy.discretization" {
  class StructuredGrid {
    +nlay
    +nrow
    +ncol
    +top
    +botm
    +xvertices
    +yvertices
  }
}

package "hydromodpy.spatial.mesh.cartesian_grid" {
  class StructuredGridBuilder {
    +build_from_surfaces(top_surface, bottom_surface, vertical_config) StructuredGrid
  }

  class SGridMeshAdapter <<module>> {
    +extract_structured_vertices(sgrid) (x_vertices, y_vertices)
    +build_field_mesh_from_sgrid(sgrid) StructuredFieldMesh
  }

  class SGridFieldParamDiscretizer <<module>> {
    +discretize_fieldparam_on_sgrid(geology_field, field_param, sgrid, ...) SGridFieldParamDiscretizationResult
    +_compute_layer_center_depths(sgrid) np.ndarray
  }

  class SGridFieldParamDiscretizationResult {
    +values_3d: np.ndarray
    +values_2d: np.ndarray
    +mesh: StructuredFieldMesh
    +field_discretization: FieldDiscretization
  }
}

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

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

package "hydromodpy.spatial.field.cases.square.field_mesh_square" {
  class StructuredFieldMesh
}

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

  abstract class FieldDiscretization {
    +mesh: BaseFieldMesh
    +field_id: str
    +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]
  }
}

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

BaseFieldMesh <|-- StructuredFieldMesh

StructuredGridBuilder --> StructuredGrid : builds
SGridMeshAdapter ..> StructuredGrid : reads XY vertices and dims
SGridMeshAdapter --> StructuredFieldMesh : builds 2D mesh view

Field --> FieldDiscretization : on_mesh(...)
FieldDiscretization <|-- WeightedAverageFieldDiscretization
FieldDiscretization --> BaseFieldMesh : mesh

FieldParam ..> FieldDiscretization : consumes weighted_components()
FieldParam --> MeshWithValues : to_mesh_field(...)

SGridFieldParamDiscretizer ..> SGridMeshAdapter : uses
SGridFieldParamDiscretizer ..> StructuredGrid : reads top/botm + dims
SGridFieldParamDiscretizer ..> Field : geology projection
SGridFieldParamDiscretizer ..> FieldParam : value mapping
SGridFieldParamDiscretizer --> SGridFieldParamDiscretizationResult : returns

SGridFieldParamDiscretizationResult *-- StructuredFieldMesh : mesh
SGridFieldParamDiscretizationResult --> FieldDiscretization : field_discretization

@enduml

Activity view:

@startuml
title Spatial Discretization - Activity (SGrid to FieldParam 3D values)

start

:Validate runtime contracts\n(support_field, field_param, sgrid);

if (Heterogeneous and strict id match?) then (yes)
  :Compare field_param.field_spatial_id\nwith support_field.identifier;
  if (Mismatch?) then (yes)
    :Raise ValueError;
    stop
  endif
endif

:Build planar mesh view from solver grid\nbuild_field_mesh_from_sgrid(sgrid);
:Project support field on this mesh\nfield_discretization = support_field.on_mesh(...);

:Compute planar reference map\nvalues_2d = field_param.to_mesh_field(..., depth);

:Read sgrid dimensions\n(nlay, nrow, ncol);
:Compute layer-center depths from top/botm\n+ global depth offset;
:Allocate values_3d[nlay, nrow, ncol];

:For each layer:\n1) extract depth map\n2) evaluate field_param.to_mesh_field(..., depth=layer_depth)\n3) assign values_3d[layer, :, :];

:Return SGridFieldParamDiscretizationResult\n(values_3d, values_2d, mesh, field_discretization);
stop

@enduml

See also#