Core Modules#
The core modules provide the fundamental data structures for representing IWFM models, including meshes, stratigraphy, and time series data.
Mesh Module#
The mesh module contains classes for representing finite element meshes,
including Node, Element, Face, and AppGrid.
Mesh classes for IWFM model representation.
This module provides the core mesh data structures including:
Node: Mesh vertices with coordinates and connectivityElement: Triangular or quadrilateral finite elementsFace: Element edges for flux calculationsSubregion: Named groups of elementsAppGrid: The complete mesh container (mirrors IWFM’s Class_AppGrid)
Example
Create a simple triangular mesh:
>>> from pyiwfm.core.mesh import AppGrid, Node, Element
>>> nodes = {
... 1: Node(id=1, x=0.0, y=0.0),
... 2: Node(id=2, x=100.0, y=0.0),
... 3: Node(id=3, x=50.0, y=100.0),
... }
>>> elements = {
... 1: Element(id=1, vertices=(1, 2, 3), subregion=1),
... }
>>> grid = AppGrid(nodes=nodes, elements=elements)
>>> grid.compute_connectivity()
>>> print(f"Mesh: {grid.n_nodes} nodes, {grid.n_elements} elements")
Mesh: 3 nodes, 1 elements
- class pyiwfm.core.mesh.Node(id, x, y, area=0.0, is_boundary=False, connected_nodes=<factory>, surrounding_elements=<factory>)[source]#
Bases:
objectA mesh node (vertex) with coordinates and connectivity information.
- Parameters:
id (
int) – Unique node identifier (1-based in IWFM).x (
float) – X coordinate in model units.y (
float) – Y coordinate in model units.area (
float, optional) – Node area for water budget calculations (Voronoi-like area). Default is 0.0, computed byAppGrid.compute_areas().is_boundary (
bool, optional) – True if node is on the model boundary. Default is False, computed byAppGrid.compute_connectivity().connected_nodes (
listofint, optional) – IDs of directly connected nodes. Computed automatically.surrounding_elements (
listofint, optional) – IDs of elements containing this node. Computed automatically.
Examples
Create a simple node:
>>> node = Node(id=1, x=100.0, y=200.0) >>> print(f"Node {node.id} at ({node.x}, {node.y})") Node 1 at (100.0, 200.0)
Create a boundary node:
>>> boundary_node = Node(id=2, x=0.0, y=0.0, is_boundary=True) >>> boundary_node.is_boundary True
Calculate distance between nodes:
>>> n1 = Node(id=1, x=0.0, y=0.0) >>> n2 = Node(id=2, x=3.0, y=4.0) >>> n1.distance_to(n2) 5.0
- __init__(id, x, y, area=0.0, is_boundary=False, connected_nodes=<factory>, surrounding_elements=<factory>)#
- class pyiwfm.core.mesh.Element(id, vertices, subregion=0, area=0.0)[source]#
Bases:
objectA finite element (triangle or quadrilateral).
IWFM supports both triangular (3 vertices) and quadrilateral (4 vertices) elements. Vertices must be ordered counter-clockwise.
- Parameters:
id (
int) – Unique element identifier (1-based in IWFM).vertices (
tupleofint) – Node IDs defining the element (3 or 4 nodes, counter-clockwise order).subregion (
int, optional) – Subregion ID this element belongs to. Default is 0 (unassigned).area (
float, optional) – Element area in coordinate units squared. Default is 0.0, computed byAppGrid.compute_areas().
- Raises:
MeshError – If number of vertices is not 3 or 4.
Examples
Create a triangular element:
>>> tri = Element(id=1, vertices=(1, 2, 3), subregion=1) >>> tri.is_triangle True >>> tri.n_vertices 3
Create a quadrilateral element:
>>> quad = Element(id=2, vertices=(1, 2, 5, 4), subregion=1) >>> quad.is_quad True >>> quad.n_vertices 4
Get element edges:
>>> tri = Element(id=1, vertices=(1, 2, 3)) >>> tri.edges [(1, 2), (2, 3), (3, 1)]
- property edges: list[tuple[int, int]]#
Return list of edge tuples (node1_id, node2_id).
Edges are returned in order around the element, closing back to first vertex.
- __init__(id, vertices, subregion=0, area=0.0)#
- class pyiwfm.core.mesh.Face(id, nodes, elements)[source]#
Bases:
objectAn element face (edge) shared between elements.
Faces are used for flux calculations between elements.
- Variables:
- __init__(id, nodes, elements)#
- class pyiwfm.core.mesh.Subregion(id, name='')[source]#
Bases:
objectA named group of elements for reporting purposes.
- Variables:
- __init__(id, name='')#
- class pyiwfm.core.mesh.AppGrid(nodes, elements=<factory>, faces=<factory>, subregions=<factory>, _x_cache=None, _y_cache=None, _vertex_cache=None, _node_id_to_idx=None)[source]#
Bases:
objectThe complete finite element mesh for an IWFM model.
This class mirrors IWFM’s Class_AppGrid and contains all mesh geometry, topology, and connectivity information.
- Parameters:
Examples
Create a 2x2 quad mesh:
>>> from pyiwfm.core.mesh import AppGrid, Node, Element >>> nodes = { ... 1: Node(id=1, x=0.0, y=0.0), ... 2: Node(id=2, x=100.0, y=0.0), ... 3: Node(id=3, x=200.0, y=0.0), ... 4: Node(id=4, x=0.0, y=100.0), ... 5: Node(id=5, x=100.0, y=100.0), ... 6: Node(id=6, x=200.0, y=100.0), ... } >>> elements = { ... 1: Element(id=1, vertices=(1, 2, 5, 4), subregion=1), ... 2: Element(id=2, vertices=(2, 3, 6, 5), subregion=1), ... } >>> grid = AppGrid(nodes=nodes, elements=elements) >>> grid.compute_connectivity() >>> grid.n_nodes 6 >>> grid.n_elements 2
Iterate over nodes:
>>> for node in grid.iter_nodes(): ... print(f"Node {node.id}: ({node.x}, {node.y})") Node 1: (0.0, 0.0) ...
Get bounding box:
>>> xmin, ymin, xmax, ymax = grid.bounding_box >>> print(f"Extent: ({xmin}, {ymin}) to ({xmax}, {ymax})") Extent: (0.0, 0.0) to (200.0, 100.0)
- property x: ndarray[tuple[Any, ...], dtype[float64]]#
Return x coordinates as numpy array, sorted by node ID.
- property y: ndarray[tuple[Any, ...], dtype[float64]]#
Return y coordinates as numpy array, sorted by node ID.
- property vertex: ndarray[tuple[Any, ...], dtype[int32]]#
Return vertex connectivity array.
Returns array of shape (n_elements, max_vertices) where max_vertices=4. For triangles, the 4th vertex is 0 (IWFM convention). Values are 0-based indices into the node arrays.
- property bounding_box: tuple[float, float, float, float]#
Return bounding box as (xmin, ymin, xmax, ymax).
- get_elements_by_subregion()[source]#
Group all elements by their subregion.
- Returns:
Dictionary mapping subregion ID to list of element IDs. Elements with subregion=0 are grouped under key 0.
- Return type:
dict[int,list[int]]
- get_subregion_areas()[source]#
Compute total area for each subregion.
- Returns:
Dictionary mapping subregion ID to total area.
- Return type:
dict[int,float]
- get_element_areas_array()[source]#
Get array of element areas in element ID order.
- Returns:
Array of areas where index i corresponds to element (i+1).
- Return type:
NDArray
- compute_connectivity()[source]#
Compute node-to-element and node-to-node connectivity.
This populates: - Node.surrounding_elements: Elements containing each node - Node.connected_nodes: Nodes directly connected via element edges - Node.is_boundary: True if node is on boundary
- build_faces()[source]#
Build face (edge) data structure from elements.
Each unique edge becomes a Face with references to adjacent elements.
- compute_areas()[source]#
Compute element areas and node areas.
Element areas are computed using the shoelace formula. Node areas are computed by distributing element area equally to vertices.
- __init__(nodes, elements=<factory>, faces=<factory>, subregions=<factory>, _x_cache=None, _y_cache=None, _vertex_cache=None, _node_id_to_idx=None)#
Stratigraphy Module#
The stratigraphy module contains classes for representing model layer structure.
Stratigraphy class for IWFM model representation.
This module provides the Stratigraphy class which represents the vertical layering structure of an IWFM groundwater model. It mirrors IWFM’s Class_Stratigraphy.
- class pyiwfm.core.stratigraphy.Stratigraphy(n_layers, n_nodes, gs_elev, top_elev, bottom_elev, active_node)[source]#
Bases:
objectVertical layering structure for an IWFM groundwater model.
This class stores the elevation data for each node at each layer, including ground surface elevation and active/inactive flags.
- Variables:
n_layers (int) – Number of aquifer layers
n_nodes (int) – Number of nodes in the mesh
gs_elev (NDArray[np.float64]) – Ground surface elevation at each node (n_nodes,)
top_elev (NDArray[np.float64]) – Top elevation of each layer at each node (n_nodes, n_layers)
bottom_elev (NDArray[np.float64]) – Bottom elevation of each layer at each node (n_nodes, n_layers)
active_node (NDArray[np.bool_]) – Whether each node is active in each layer (n_nodes, n_layers)
- gs_elev: NDArray[np.float64]#
- top_elev: NDArray[np.float64]#
- bottom_elev: NDArray[np.float64]#
- active_node: NDArray[np.bool_]#
- get_layer_thickness(layer)[source]#
Get thickness of a layer at all nodes.
- Parameters:
layer (int) – Layer index (0-based)
- Returns:
Array of thickness values at each node
- Return type:
NDArray[np.float64]
- get_total_thickness()[source]#
Get total thickness of all layers at each node.
- Returns:
Array of total thickness values at each node
- Return type:
NDArray[np.float64]
- get_layer_top(layer)[source]#
Get top elevation of a layer at all nodes.
- Parameters:
layer (int) – Layer index (0-based)
- Returns:
Array of top elevation values at each node
- Return type:
NDArray[np.float64]
- get_layer_bottom(layer)[source]#
Get bottom elevation of a layer at all nodes.
- Parameters:
layer (int) – Layer index (0-based)
- Returns:
Array of bottom elevation values at each node
- Return type:
NDArray[np.float64]
- get_elevation_at_depth(node_idx, depth)[source]#
Get elevation at a given depth below ground surface.
- copy()[source]#
Create a deep copy of the stratigraphy.
- Returns:
New Stratigraphy object with copied data
- Return type:
- __init__(n_layers, n_nodes, gs_elev, top_elev, bottom_elev, active_node)#
Time Series Module#
The time series module provides classes for working with temporal data.
Time series classes for IWFM model data.
This module provides classes for representing time steps and time series data used throughout IWFM models.
- class pyiwfm.core.timeseries.TimeUnit(*values)[source]#
Bases:
EnumTime units supported by IWFM.
- MINUTE = 'MIN'#
- HOUR = 'HOUR'#
- DAY = 'DAY'#
- WEEK = 'WEEK'#
- MONTH = 'MON'#
- YEAR = 'YEAR'#
- class pyiwfm.core.timeseries.TimeStep(start, end, index=0)[source]#
Bases:
objectA single time step in an IWFM simulation.
- Variables:
start (datetime.datetime) – Start datetime of the time step
end (datetime.datetime) – End datetime of the time step
index (int) – Time step index (0-based)
- __init__(start, end, index=0)#
- class pyiwfm.core.timeseries.SimulationPeriod(start, end, time_step_length, time_step_unit)[source]#
Bases:
objectDefines the simulation time period for an IWFM model.
- Variables:
start (datetime.datetime) – Simulation start datetime
end (datetime.datetime) – Simulation end datetime
time_step_length (int) – Length of each time step
time_step_unit (pyiwfm.core.timeseries.TimeUnit) – Unit of the time step length
- __init__(start, end, time_step_length, time_step_unit)#
- class pyiwfm.core.timeseries.TimeSeries(times, values, name='', units='', location='', metadata=<factory>)[source]#
Bases:
objectA time series of values.
- Variables:
times (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.datetime64]]) – Array of datetime objects or timestamps
values (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – Array of values (can be 1D or 2D)
name (str) – Name/identifier for this time series
units (str) – Units of the values
location (str) – Location identifier (e.g., node ID, element ID)
- property start_time: datetime64#
Return start time.
- property end_time: datetime64#
Return end time.
- classmethod from_datetimes(times, values, **kwargs)[source]#
Create a TimeSeries from Python datetime objects.
- to_dataframe()[source]#
Convert to a pandas DataFrame.
- Returns:
DataFrame with times as index
- Return type:
- resample(freq)[source]#
Resample the time series to a new frequency.
- Parameters:
freq (str) – Pandas frequency string (e.g., ‘D’ for daily, ‘M’ for monthly)
- Returns:
New resampled TimeSeries
- Return type:
- slice_time(start=None, end=None)[source]#
Slice the time series to a time range.
- Parameters:
- Returns:
New sliced TimeSeries
- Return type:
- __init__(times, values, name='', units='', location='', metadata=<factory>)#
- class pyiwfm.core.timeseries.TimeSeriesCollection(series=<factory>, name='', variable='')[source]#
Bases:
objectA collection of related time series (e.g., heads at multiple nodes).
- Variables:
series (dict[str, pyiwfm.core.timeseries.TimeSeries]) – Dictionary mapping location ID to TimeSeries
name (str) – Name of the collection
variable (str) – Variable name (e.g., ‘head’, ‘flow’)
- series: dict[str, TimeSeries]#
- __init__(series=<factory>, name='', variable='')#
Base Component#
Abstract base class that all model components (groundwater, streams, lakes, root zone, small watersheds, unsaturated zone) inherit from, providing a consistent interface for validation and item counting.
Abstract base class for IWFM model components.
All major IWFM simulation components (groundwater, streams, lakes,
root zone, small watersheds, unsaturated zone) share a common
interface defined by BaseComponent. This enables generic
model validation, iteration, and serialization.
- class pyiwfm.core.base_component.BaseComponent[source]#
Bases:
ABCAbstract base class for IWFM model components.
Every component must implement:
validate()– return a list of validation error strings (empty list means valid).n_items– number of primary entities managed by the component (e.g., number of wells, stream nodes, lakes).
Model Module#
The model module provides the central IWFMModel class that orchestrates all
model components.
Loading Models:
IWFMModel.from_preprocessor(pp_file)- Load from preprocessor input files (mesh, stratigraphy, geometry)IWFMModel.from_preprocessor_binary(binary_file)- Load from preprocessor binary outputIWFMModel.from_simulation(sim_file)- Load complete model from simulation input fileIWFMModel.from_simulation_with_preprocessor(sim_file, pp_file)- Load using both filesIWFMModel.from_hdf5(hdf5_file)- Load from HDF5 file
Saving Models:
model.to_preprocessor(output_dir)- Save to preprocessor input filesmodel.to_simulation(output_dir)- Save complete model to simulation filesmodel.to_hdf5(output_file)- Save to HDF5 formatmodel.to_binary(output_file)- Save mesh/stratigraphy to binary
IWFMModel class - main orchestrator for IWFM model components.
This module provides the central IWFMModel class that orchestrates all model components including mesh, stratigraphy, groundwater, streams, lakes, and root zone.
- class pyiwfm.core.model.IWFMModel(name, mesh=None, stratigraphy=None, groundwater=None, streams=None, lakes=None, rootzone=None, small_watersheds=None, unsaturated_zone=None, supply_adjustment=None, metadata=<factory>, source_files=<factory>)[source]#
Bases:
objectThe main IWFM model container class.
This class orchestrates all model components and provides methods for reading, writing, and validating IWFM models. It mirrors the structure of IWFM’s Package_Model.
- Variables:
name (str) – Model name/identifier
mesh (AppGrid | None) – Finite element mesh (AppGrid)
stratigraphy (Stratigraphy | None) – Vertical layering structure
groundwater (AppGW | None) – Groundwater component (AppGW) - wells, pumping, BCs, aquifer params
streams (AppStream | None) – Stream network component (AppStream) - nodes, reaches, diversions, bypasses
lakes (AppLake | None) – Lake component (AppLake) - lake definitions, elements, rating curves
rootzone (RootZone | None) – Root zone component (RootZone) - crop types, soil params, land use
small_watersheds (AppSmallWatershed | None) – Small Watershed component (AppSmallWatershed)
unsaturated_zone (AppUnsatZone | None) – Unsaturated Zone component (AppUnsatZone)
supply_adjustment (SupplyAdjustment | None) – Parsed supply adjustment specification data
- stratigraphy: Stratigraphy | None = None#
- small_watersheds: AppSmallWatershed | None = None#
- unsaturated_zone: AppUnsatZone | None = None#
- supply_adjustment: SupplyAdjustment | None = None#
- classmethod from_preprocessor(pp_file, load_streams=True, load_lakes=True)[source]#
Load a model from PreProcessor input files.
This loads the model structure (mesh, stratigraphy) and optionally the stream and lake geometry from the preprocessor input file and all files referenced by it.
Note: This creates a “partial” model with only the static geometry defined in the preprocessor. It does not include dynamic components like groundwater parameters, pumping, or root zone data which are defined in the simulation input files.
- Parameters:
- Returns:
IWFMModel instance with mesh, stratigraphy, and optionally streams/lakes geometry loaded
- Return type:
Example
>>> model = IWFMModel.from_preprocessor("Preprocessor/Preprocessor.in") >>> print(f"Loaded {model.n_nodes} nodes, {model.n_elements} elements")
- classmethod from_preprocessor_binary(binary_file, name='')[source]#
Load a model from the native IWFM PreProcessor binary output.
The preprocessor binary file (
ACCESS='STREAM') contains mesh, stratigraphy, stream/lake connectors, and component data compiled by the IWFM PreProcessor.- Parameters:
- Returns:
IWFMModel with mesh, stratigraphy, streams, and lakes loaded
- Return type:
Example
>>> model = IWFMModel.from_preprocessor_binary("PreprocessorOut.bin") >>> print(f"Loaded {model.n_nodes} nodes, {model.n_layers} layers")
- classmethod from_simulation(simulation_file)[source]#
Load a complete IWFM model from a simulation main input file.
Delegates to
CompleteModelLoaderwhich auto-detects the simulation file format and loads all components (mesh, stratigraphy, groundwater, streams, lakes, root zone, etc.).- Parameters:
simulation_file (Path | str) – Path to the simulation main input file
- Returns:
IWFMModel instance with all components loaded
- Return type:
Example
>>> model = IWFMModel.from_simulation("Simulation/Simulation.in") >>> print(f"Stream nodes: {len(model.streams.nodes)}")
- classmethod from_simulation_with_preprocessor(simulation_file, preprocessor_file, load_timeseries=False)[source]#
Load a complete IWFM model using both simulation and preprocessor files.
This method first loads the mesh and stratigraphy from the preprocessor input file (ASCII format), then loads all dynamic components from the simulation input file and its referenced component files.
Use this method when: - You have both preprocessor input files and simulation input files - You want to load from ASCII preprocessor files rather than binary - The binary file path in the simulation file is incorrect or missing
- Parameters:
- Returns:
IWFMModel instance with all components loaded
- Return type:
Example
>>> model = IWFMModel.from_simulation_with_preprocessor( ... "Simulation/Simulation.in", ... "Preprocessor/Preprocessor.in" ... )
- classmethod from_hdf5(hdf5_file)[source]#
Load a model from HDF5 output file.
This loads a complete model that was previously saved to HDF5 format using the to_hdf5() method or write_model_hdf5() function.
- Parameters:
- Returns:
Loaded IWFMModel instance
- Return type:
Example
>>> model = IWFMModel.from_hdf5("model.h5")
- to_preprocessor(output_dir)[source]#
Write model to PreProcessor input files.
Creates all preprocessor input files (nodes, elements, stratigraphy) in the specified output directory.
- to_simulation(output_dir, file_paths=None, ts_format='text')[source]#
Write complete model to simulation input files.
Creates all input files required for an IWFM simulation, including preprocessor files, component files, and the simulation control file.
- Parameters:
- Returns:
Dictionary mapping file type to output path
- Return type:
- to_hdf5(output_file)[source]#
Write model to HDF5 file.
Saves the complete model (mesh, stratigraphy, and all components) to a single HDF5 file for efficient storage and later loading.
Example
>>> model.to_hdf5("model.h5")
- summary()[source]#
Return a summary string of the model.
- Returns:
Multi-line summary of model components
- Return type:
- __init__(name, mesh=None, stratigraphy=None, groundwater=None, streams=None, lakes=None, rootzone=None, small_watersheds=None, unsaturated_zone=None, supply_adjustment=None, metadata=<factory>, source_files=<factory>)#
Model Factory#
Helper functions extracted from IWFMModel for model construction:
reach building from node reach IDs, stream node coordinate resolution,
parametric grid application, KH anomaly application, subsidence parameters,
and binary-to-model conversion. IWFMModel classmethods delegate to
these functions.
Factory helper functions for constructing IWFMModel instances.
This module contains the model-building helper functions extracted from model.py to keep the IWFMModel class focused on data and properties. The classmethods on IWFMModel delegate to these functions.
- pyiwfm.core.model_factory.build_reaches_from_node_reach_ids(stream)[source]#
Build StrmReach objects by grouping stream nodes by their reach_id.
Only populates
stream.reacheswhen it is currently empty and at least some nodes carry a non-zeroreach_id.- Parameters:
stream (
AppStream) – Stream network whosenodesare inspected.
- pyiwfm.core.model_factory.apply_kh_anomalies(params, anomalies, mesh)[source]#
Apply Kh anomaly overwrites from element-level data to node arrays.
For each anomaly element, sets Kh at all vertex nodes to the anomaly value. This matches IWFM Fortran behavior in
ReadAquiferParameters(Class_AppGW.f90:4433–4442).- Parameters:
params (
AquiferParameters) – Aquifer parameters whosekharray will be modified in-place.anomalies (
list[KhAnomalyEntry]) – Parsed anomaly entries from the GW main file.mesh (
AppGrid) – Model mesh providing element-to-node connectivity.
- Returns:
Number of anomalies successfully applied.
- Return type:
- pyiwfm.core.model_factory.apply_parametric_grids(gw, parametric_grids, mesh)[source]#
Interpolate aquifer parameters from parametric grids onto model nodes.
Builds a
ParametricGridfor each group, interpolates all 5 parameters (Kh, Ss, Sy, AquitardKv, Kv) at every model node, and assigns the result as aquifer parameters.Returns
Trueif interpolation was performed.
- pyiwfm.core.model_factory.apply_parametric_subsidence(subs_config, mesh, n_nodes, n_layers)[source]#
Interpolate subsidence parameters from parametric grids onto model nodes.
Returns a list of SubsidenceNodeParams objects.
- pyiwfm.core.model_factory.binary_data_to_model(data, name='')[source]#
Convert
PreprocessorBinaryDatato anIWFMModel.Builds Node, Element, Subregion, Stratigraphy, AppStream, and AppLake objects from the raw arrays in data.
- pyiwfm.core.model_factory.resolve_stream_node_coordinates(model)[source]#
Resolve stream node (0,0) coordinates from associated GW nodes.
Many IWFM loaders create stream nodes with placeholder
(0, 0)coordinates. When the stream node has agw_nodereference we can look up the real coordinates from the mesh.Returns the number of nodes updated.
Zones Module#
The zones module provides data structures for defining spatial zones (subregions, custom zones for ZBudget analysis) and mapping elements to zones.
Zone definitions for multi-scale data viewing.
This module provides zone data structures for spatial aggregation:
Zone: A named group of elements with computed areaZoneDefinition: Collection of zones with element-to-zone mapping
Zone definitions support multiple spatial scales: - Subregions (predefined groupings from model input files) - User-specified zones (custom groupings for ZBudget analysis)
Example
Create a simple zone definition:
>>> from pyiwfm.core.zones import Zone, ZoneDefinition
>>> import numpy as np
>>> zones = {
... 1: Zone(id=1, name="North Basin", elements=[1, 2, 3], area=1500.0),
... 2: Zone(id=2, name="South Basin", elements=[4, 5, 6], area=1200.0),
... }
>>> element_zones = np.array([1, 1, 1, 2, 2, 2]) # element_id-1 -> zone_id
>>> zone_def = ZoneDefinition(zones=zones, extent="horizontal", element_zones=element_zones)
>>> zone_def.get_zone_for_element(3)
1
>>> zone_def.get_elements_in_zone(2)
[4, 5, 6]
- class pyiwfm.core.zones.Zone(id, name, elements=<factory>, area=0.0)[source]#
Bases:
objectA named zone containing a group of elements.
- Parameters:
id (
int) – Unique zone identifier (1-based).name (
str) – Descriptive name for the zone (e.g., “Sacramento Valley”).elements (
listofint) – List of element IDs belonging to this zone.area (
float, optional) – Total zone area in model units squared. Default is 0.0, can be computed from element areas.
Examples
Create a zone:
>>> zone = Zone(id=1, name="Central Basin", elements=[10, 11, 12, 13], area=5000.0) >>> print(f"Zone {zone.id}: {zone.name} ({len(zone.elements)} elements)") Zone 1: Central Basin (4 elements)
Check if element is in zone:
>>> 11 in zone.elements True
- __init__(id, name, elements=<factory>, area=0.0)#
- class pyiwfm.core.zones.ZoneDefinition(zones=<factory>, extent='horizontal', element_zones=None, name='', description='')[source]#
Bases:
objectA complete zone definition with element-to-zone mapping.
- Parameters:
zones (
dict[int,Zone]) – Dictionary mapping zone ID to Zone object.extent (
str, optional) – Zone extent type: “horizontal” (default) or “vertical”. Horizontal zones span all layers; vertical zones are layer-specific.element_zones (
NDArray, optional) – Array mapping element index (0-based) to zone ID. Shape: (n_elements,). Elements with no zone have value 0.name (
str, optional) – Name for this zone definition set.description (
str, optional) – Description of the zone definition.
Examples
Create a zone definition from scratch:
>>> zones = { ... 1: Zone(id=1, name="Zone A", elements=[1, 2, 3]), ... 2: Zone(id=2, name="Zone B", elements=[4, 5]), ... } >>> elem_zones = np.array([1, 1, 1, 2, 2]) >>> zone_def = ZoneDefinition(zones=zones, element_zones=elem_zones) >>> zone_def.n_zones 2
Create from subregions:
>>> from pyiwfm.core.mesh import AppGrid, Subregion >>> grid = ... # Load from model >>> zone_def = ZoneDefinition.from_subregions(grid)
- classmethod from_subregions(grid)[source]#
Create a ZoneDefinition from model subregions.
- Parameters:
grid (
AppGrid) – The model grid with subregion assignments.- Returns:
Zone definition with one zone per subregion.
- Return type:
- classmethod from_element_list(element_zone_pairs, zone_names=None, element_areas=None, name='', description='')[source]#
Create a ZoneDefinition from a list of (element_id, zone_id) pairs.
- Parameters:
element_zone_pairs (
listof(int,int)) – List of (element_id, zone_id) pairs.zone_names (
dict[int,str], optional) – Mapping of zone ID to zone name.element_areas (
dict[int,float], optional) – Mapping of element ID to element area.name (
str, optional) – Name for this zone definition.description (
str, optional) – Description of the zone definition.
- Returns:
The constructed zone definition.
- Return type:
- add_zone(zone)[source]#
Add a new zone to the definition.
- Parameters:
zone (
Zone) – The zone to add.
- compute_areas(grid)[source]#
Recompute zone areas from element areas.
- Parameters:
grid (
AppGrid) – The model grid with computed element areas.
- __init__(zones=<factory>, extent='horizontal', element_zones=None, name='', description='')#
Aggregation Module#
The aggregation module provides spatial data aggregation from element-level values to zone-level statistics using configurable methods.
Spatial data aggregation for multi-scale viewing.
This module provides aggregation utilities for computing zone-level values from element-level data:
DataAggregator: Main aggregation engine with multiple methods
Supported aggregation methods: - sum: Total value across zone - mean: Simple average - area_weighted_mean: Area-weighted average (default) - min: Minimum value - max: Maximum value - median: Median value
Example
Aggregate element values to zones:
>>> from pyiwfm.core.aggregation import DataAggregator
>>> from pyiwfm.core.zones import ZoneDefinition
>>> import numpy as np
>>> aggregator = DataAggregator()
>>> element_values = np.array([10.0, 12.0, 11.0, 20.0, 22.0]) # 5 elements
>>> zone_def = ... # ZoneDefinition with 2 zones
>>> zone_values = aggregator.aggregate(element_values, zone_def, method="mean")
>>> print(zone_values)
{1: 11.0, 2: 21.0}
- class pyiwfm.core.aggregation.AggregationMethod(*values)[source]#
Bases:
EnumAvailable aggregation methods.
- SUM = 'sum'#
- MEAN = 'mean'#
- AREA_WEIGHTED_MEAN = 'area_weighted_mean'#
- MIN = 'min'#
- MAX = 'max'#
- MEDIAN = 'median'#
- class pyiwfm.core.aggregation.DataAggregator(element_areas=None)[source]#
Bases:
objectAggregates element-level data to zone-level values.
- Parameters:
element_areas (
NDArray, optional) – Array of element areas for area-weighted calculations. Shape: (n_elements,). Required for area_weighted_mean.
Examples
Create an aggregator with element areas:
>>> areas = np.array([100.0, 150.0, 200.0, 125.0, 175.0]) >>> aggregator = DataAggregator(element_areas=areas)
Aggregate hydraulic head values:
>>> head_values = np.array([50.0, 52.0, 48.0, 60.0, 62.0]) >>> zone_def = ... # Zone 1: elements 1-3, Zone 2: elements 4-5 >>> zone_heads = aggregator.aggregate(head_values, zone_def, method="area_weighted_mean")
- aggregate(values, zone_def, method='area_weighted_mean')[source]#
Aggregate element values to zone-level values.
- Parameters:
values (
NDArray) – Element-level values. Shape: (n_elements,).zone_def (
ZoneDefinition) – Zone definition with element-to-zone mapping.method (
str, optional) – Aggregation method. One of: “sum”, “mean”, “area_weighted_mean”, “min”, “max”, “median”. Default is “area_weighted_mean”.
- Returns:
Dictionary mapping zone ID to aggregated value.
- Return type:
dict[int,float]- Raises:
ValueError – If method is not recognized or areas not provided for area_weighted_mean.
Examples
>>> aggregator = DataAggregator(element_areas=areas) >>> result = aggregator.aggregate(heads, zone_def, method="mean") >>> print(f"Zone 1 mean head: {result[1]:.2f}")
- aggregate_to_array(values, zone_def, method='area_weighted_mean')[source]#
Aggregate and expand zone values back to element array.
Each element gets its zone’s aggregated value. Useful for visualization where you want to color elements by their zone’s aggregate value.
- Parameters:
values (
NDArray) – Element-level values. Shape: (n_elements,).zone_def (
ZoneDefinition) – Zone definition with element-to-zone mapping.method (
str, optional) – Aggregation method. Default is “area_weighted_mean”.
- Returns:
Array of same shape as input, with each element’s value replaced by its zone’s aggregated value. Elements with no zone get NaN.
- Return type:
NDArray
Examples
>>> expanded = aggregator.aggregate_to_array(heads, zone_def) >>> # expanded[0:3] all have same value (Zone 1's aggregate)
- aggregate_timeseries(timeseries_values, zone_def, method='area_weighted_mean')[source]#
Aggregate a time series of element values to zone-level time series.
- Parameters:
- Returns:
Dictionary mapping zone ID to list of aggregated values over time.
- Return type:
dict[int,list[float]]
Examples
>>> head_series = [heads_t0, heads_t1, heads_t2] # 3 timesteps >>> zone_series = aggregator.aggregate_timeseries(head_series, zone_def) >>> print(f"Zone 1 heads over time: {zone_series[1]}")
- pyiwfm.core.aggregation.create_aggregator_from_grid(grid)[source]#
Create a DataAggregator with element areas from an AppGrid.
- Parameters:
grid (
AppGrid) – The model grid with computed element areas.- Returns:
Configured aggregator ready for area-weighted calculations.
- Return type:
Examples
>>> from pyiwfm.core.mesh import AppGrid >>> grid = ... # Load from model >>> aggregator = create_aggregator_from_grid(grid) >>> zone_values = aggregator.aggregate(heads, zone_def, "area_weighted_mean")
Mesh Quality#
Compute element-level quality metrics (aspect ratio, skewness, min/max angle) and aggregate statistics for the entire mesh.
Mesh quality metrics for IWFM finite element meshes.
This module computes per-element quality metrics (area, aspect ratio, skewness, interior angles) and aggregate statistics for an entire mesh. Useful for identifying poorly shaped elements that may cause numerical issues.
Example
Compute quality for a single triangle:
>>> verts = [(0.0, 0.0), (1.0, 0.0), (0.5, 0.866)]
>>> q = compute_element_quality(verts, element_id=1)
>>> q.n_vertices
3
>>> q.min_angle > 59.0
True
Compute mesh-wide quality report:
>>> from pyiwfm.core.mesh import AppGrid, Node, Element
>>> nodes = {1: Node(1, 0, 0), 2: Node(2, 1, 0), 3: Node(3, 1, 1), 4: Node(4, 0, 1)}
>>> elements = {1: Element(1, (1, 2, 3, 4))}
>>> grid = AppGrid(nodes=nodes, elements=elements)
>>> report = compute_mesh_quality(grid)
>>> report.n_elements
1
- class pyiwfm.core.mesh_quality.ElementQuality(element_id, area, aspect_ratio, skewness, min_angle, max_angle, n_vertices)[source]#
Bases:
objectQuality metrics for a single mesh element.
- Parameters:
element_id (
int) – Element identifier (1-based).area (
float) – Element area computed via the shoelace formula.aspect_ratio (
float) – Ratio of the longest edge to the shortest edge. A value of 1.0 indicates all edges are equal length.skewness (
float) – Deviation from an ideal shape, ranging from 0.0 (perfect) to 1.0 (degenerate). For triangles, compares to equilateral; for quads, compares diagonal lengths.min_angle (
float) – Smallest interior angle in degrees.max_angle (
float) – Largest interior angle in degrees.n_vertices (
int) – Number of vertices (3 for triangle, 4 for quad).
- __init__(element_id, area, aspect_ratio, skewness, min_angle, max_angle, n_vertices)#
- class pyiwfm.core.mesh_quality.MeshQualityReport(n_elements, n_triangles, n_quads, area_min, area_max, area_mean, area_std, aspect_ratio_min, aspect_ratio_max, aspect_ratio_mean, skewness_mean, min_angle_global, max_angle_global, poor_quality_count, element_qualities=<factory>)[source]#
Bases:
objectAggregate mesh quality statistics.
- Parameters:
n_elements (
int) – Total number of elements.n_triangles (
int) – Number of triangular elements.n_quads (
int) – Number of quadrilateral elements.area_min (
float) – Minimum element area.area_max (
float) – Maximum element area.area_mean (
float) – Mean element area.area_std (
float) – Standard deviation of element areas.aspect_ratio_min (
float) – Minimum aspect ratio across all elements.aspect_ratio_max (
float) – Maximum aspect ratio across all elements.aspect_ratio_mean (
float) – Mean aspect ratio.skewness_mean (
float) – Mean skewness across all elements.min_angle_global (
float) – Smallest interior angle in the entire mesh (degrees).max_angle_global (
float) – Largest interior angle in the entire mesh (degrees).poor_quality_count (
int) – Number of elements with aspect_ratio > 10 or skewness > 0.8.element_qualities (
list[ElementQuality]) – Per-element quality metrics.
- element_qualities: list[ElementQuality]#
- to_dict()[source]#
Serialize for API response (without per-element details).
- Returns:
Dictionary with aggregate statistics only.
- Return type:
dict[str,Any]
- __init__(n_elements, n_triangles, n_quads, area_min, area_max, area_mean, area_std, aspect_ratio_min, aspect_ratio_max, aspect_ratio_mean, skewness_mean, min_angle_global, max_angle_global, poor_quality_count, element_qualities=<factory>)#
- pyiwfm.core.mesh_quality.compute_element_quality(vertices, element_id=0)[source]#
Compute quality metrics for a single element given its vertex coordinates.
- Parameters:
vertices (
list[tuple[float,float]]) – Ordered vertex coordinates (3 for triangle, 4 for quad).element_id (
int, optional) – Element identifier for the returned dataclass. Default is 0.
- Returns:
Quality metrics for the element.
- Return type:
- Raises:
ValueError – If the number of vertices is not 3 or 4.
- pyiwfm.core.mesh_quality.compute_mesh_quality(grid)[source]#
Compute quality metrics for the entire mesh.
Iterates over all elements, computes per-element quality metrics, and aggregates them into summary statistics.
- Parameters:
grid (
AppGrid) – The mesh to analyze.- Returns:
Aggregate and per-element quality metrics.
- Return type:
Query Module#
The query module provides a high-level API for accessing model data at multiple spatial scales with aggregation and export capabilities.
High-level query API for multi-scale data access.
This module provides a unified interface for querying model data at different spatial scales:
ModelQueryAPI: Main query interface with export capabilities
Example
Query and export model data:
>>> from pyiwfm.core.query import ModelQueryAPI
>>> api = ModelQueryAPI(model)
>>> # Get zone-aggregated values
>>> zone_heads = api.get_values("head", scale="subregion", layer=1)
>>> # Export to DataFrame
>>> df = api.export_to_dataframe(["head", "kh"], scale="subregion")
>>> df.to_csv("subregion_data.csv")
- class pyiwfm.core.query.TimeSeries(times, values, variable, location_id, location_type, units='')[source]#
Bases:
objectTime series data for a single location or zone.
- Parameters:
times (
listofdatetime) – Timestamps for each value.values (
NDArray) – Array of values at each timestamp.variable (
str) – Variable name (e.g., “head”, “pumping”).location_id (
int) – Zone, element, or node ID.location_type (
str) – Type of location: “zone”, “element”, “node”.units (
str, optional) – Units of the values.
- __init__(times, values, variable, location_id, location_type, units='')#
- class pyiwfm.core.query.ModelQueryAPI(model)[source]#
Bases:
objectHigh-level API for querying model data at multiple spatial scales.
- Parameters:
model (
IWFMModel) – The IWFM model instance.- Variables:
model (
IWFMModel) – The underlying model.aggregator (
DataAggregator) – Aggregator for zone-level calculations.
Examples
Create API and query data:
>>> from pyiwfm.core.model import IWFMModel >>> model = IWFMModel.from_preprocessor("Preprocessor_MAIN.IN") >>> api = ModelQueryAPI(model)
Get element-level values:
>>> heads = api.get_values("head", scale="element", layer=1) >>> print(f"Head at element 1: {heads[1]:.2f} ft")
Get zone-aggregated values:
>>> zone_heads = api.get_values("head", scale="subregion", layer=1) >>> for zone_id, head in zone_heads.items(): ... print(f"Zone {zone_id}: {head:.2f} ft")
Export to DataFrame:
>>> df = api.export_to_dataframe(["head", "kh"], scale="subregion") >>> df.to_csv("zone_data.csv", index=False)
- PROPERTY_INFO: dict[str, dict[str, Any]] = {'area': {'name': 'Element Area', 'source': 'mesh', 'units': 'sq ft'}, 'bottom_elev': {'name': 'Bottom Elevation', 'source': 'stratigraphy', 'units': 'ft'}, 'head': {'name': 'Hydraulic Head', 'source': 'results', 'units': 'ft'}, 'kh': {'name': 'Horizontal K', 'source': 'params', 'units': 'ft/d'}, 'kv': {'name': 'Vertical K', 'source': 'params', 'units': 'ft/d'}, 'ss': {'name': 'Specific Storage', 'source': 'params', 'units': '1/ft'}, 'subregion': {'name': 'Subregion ID', 'source': 'mesh', 'units': ''}, 'sy': {'name': 'Specific Yield', 'source': 'params', 'units': ''}, 'thickness': {'name': 'Layer Thickness', 'source': 'stratigraphy', 'units': 'ft'}, 'top_elev': {'name': 'Top Elevation', 'source': 'stratigraphy', 'units': 'ft'}}#
- property aggregator: DataAggregator#
Get or create the data aggregator.
- property subregion_zones: ZoneDefinition#
Get zone definition from model subregions.
- register_zones(name, zone_def)[source]#
Register a custom zone definition.
- Parameters:
name (
str) – Name to identify this zone definition.zone_def (
ZoneDefinition) – The zone definition to register.
- get_values(variable, scale='element', layer=None, time_index=None, aggregation='area_weighted_mean')[source]#
Get property values at specified scale.
- Parameters:
variable (
str) – Property name (e.g., “head”, “kh”, “area”).scale (
str, optional) – Spatial scale: “element”, “subregion”, or custom zone name. Default is “element”.layer (
int, optional) – Model layer (1-based). None for 2D properties.time_index (
int, optional) – Time index for time-varying properties.aggregation (
str, optional) – Aggregation method for zone scales. Default is “area_weighted_mean”.
- Returns:
Dictionary mapping element/zone ID to value.
- Return type:
dict[int,float]
Examples
Get element-level heads:
>>> heads = api.get_values("head", scale="element", layer=1)
Get subregion-average heads:
>>> zone_heads = api.get_values("head", scale="subregion", layer=1)
- get_timeseries(variable, location_id, scale='element', layer=None, aggregation='area_weighted_mean')[source]#
Get time series for a location at specified scale.
- Parameters:
- Returns:
Time series data, or None if not available.
- Return type:
TimeSeriesorNone
Notes
Requires model to have time-varying data loaded.
- export_to_dataframe(variables, scale='element', layer=None, time_index=None, aggregation='area_weighted_mean')[source]#
Export values to a pandas DataFrame.
- Parameters:
- Returns:
DataFrame with columns for location ID and each variable.
- Return type:
pd.DataFrame
Examples
>>> df = api.export_to_dataframe(["head", "kh", "area"], scale="subregion") >>> print(df.head())
- export_to_csv(variables, filepath, scale='element', layer=None, time_index=None, aggregation='area_weighted_mean')[source]#
Export values to a CSV file.
- Parameters:
Examples
>>> api.export_to_csv(["head", "kh"], "zone_data.csv", scale="subregion")
- export_timeseries_to_csv(variable, location_ids, filepath, scale='element', layer=None, aggregation='area_weighted_mean')[source]#
Export time series for multiple locations to CSV.