Changelog#
All notable changes to pyiwfm will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]#
Added#
Changed#
Fixed#
[1.3.0] - 2026-04-26#
Quick-win patch series surfaced by a fresh post-v1.2.0 audit. All
non-breaking; bigger refactors (Phase 3 + 4.B) bundle into v2.0 on the
next branch.
Changed#
Vectorize webapi route hot paths. Three
for i in range(...)loops over NumPy arrays were pure overhead in the webapi serving path:GET /api/results/head-diff(routes/results.py:110) now rounds and masks dry cells in onenp.round+ list comprehension over Python primitives instead of indexing element-by-element.GET /api/budgets/{type}/summary(routes/budgets.py:486) computes per-column totals and means viaaxis=0reductions instead of looping withnp.nansum/np.nanmeanper column.GET /api/export/budget-csvandGET /api/export/timeseries/budgetpre-round the entire matrix once withnp.roundand zip rows instead of rounding each cell inside two nested Python loops.
Added a byte-for-byte equivalence test (
test_diff_values_match_legacy_per_index_formula) asserting the newhead-diffrounds identically to the legacyround(float(diff[i]), 3)formula on banker’s-rounding ties and dry-cell edge cases.Replace ``”No model loaded”`` boilerplate with ``ModelState`` methods. Add
ModelState.require_loaded()andModelState.require_model()methods that pair with the existing module-levelrequire_model()helper but resolve through the caller’smodel_statebinding (sopatch("...routes.foo.model_state", state)tests substitute their fixture transparently). Replace 23 instances of the two-lineif not model_state.is_loaded: raise HTTPException(...)pattern across results / budgets / zbudgets / export / mesh / model / observations / properties routes; net –74/+59 lines.Consolidate ``_make_config_v40`` and ``_make_config_v50`` test fixtures in
test_io_gw_subsidence_writer.pyinto a single version-parameterized factory. The two original helpers shared 22 of 25 default fields verbatim.Extract ``cli/_parsers.py::add_control_file_subcommand`` for the shared
budget/zbudgetparser-registration pattern.
Notes#
@cached_propertyaudit found no conversions are warranted in v1.3.x: the expensive properties (AppGrid.x,y,vertex,element_centroids) are already hand-cached with explicit_invalidate_cache()hooks that the test suite asserts directly.n_*count properties cannot safely be cached because the underlying collections are mutated by the v1.2.0 mutation helpers. The remaining@propertydeclarations are trivial accessors where caching adds bookkeeping overhead exceeding the saved work.Cross-test-file fixture consolidation (broader than the within-file v40/v50 case above) was rejected after audit: each
_make_confighelper acrosstest_io_*_writer.pybuilds a different writer-config dataclass with unique defaults, so moving them to a shared_writer_fixtures.pywould relocate lines rather than delete them.
[1.2.0] - 2026-04-25#
Major minor release from a comprehensive code review (see
docs/V2_ROADMAP.md for what’s deferred to v2.0). Non-breaking;
existing v1.1.x code keeps working without modification.
Added#
Python API for editing loaded models (pyiwfm.core.model.IWFMModel)
IWFMModel.set_aquifer_parameter(param, layer, values)— replace an aquifer-parameter array ("kh","kv","ss","sy","aquitard_kv") for a single layer.IWFMModel.set_aquifer_parameter_at(param, node_id, layer, value)— set a single(node, layer)cell.IWFMModel.set_stratigraphy_from_thicknesses(gs_elev, aquitard_thicknesses, aquifer_thicknesses, active_node=None)— rebuild stratigraphy from thickness arrays with mesh-consistency check.IWFMModel.add_observation_well(node_id, layer, x, y, name="")andremove_observation_well(name)— manage groundwater hydrograph output locations.IWFMModel.mark_dirty(component_name)plus internal_dirty: set[str]field — track which components have been mutated since the last load/save.New user guide page
docs/user_guide/mutating_models.rstwalks through the calibration workflow.
Stream depletion analysis suite (pyiwfm.io.stream_depletion,
pyiwfm.visualization.{plot,map}_depletion, pyiwfm depletion CLI)
BudgetOutputMissingErrorraised when a model required for comparative analysis didn’t declare or didn’t produce a stream reach/node budget; message tells the operator which IWFM input line to fix (STRMRCHBUDFL/STNDBUDFL).compute_stream_depletion_from_models(baseline_model, scenario_model, *, reach_ids=None, sa_column=...)— model-driven reach-level depletion; resolves budget paths frommodel.metadata['stream_budget_file'].compute_stream_node_depletion(baseline_model, scenario_model, *, node_ids=None, sa_column=...)andStreamNodeDepletionResult/StreamNodeDepletionReport— per-stream-node analysis using the stream node budget HDF.DEFAULT_SA_COLUMNconstant ("Stream-Aquifer Interaction Within Model", the IWFM v5+ canonical column name); override viasa_column=...for older models that emit"Gain from GW (+)".Tabular writers:
write_stream_depletion_csv/write_stream_depletion_json/write_stream_depletion_excel, plusStreamDepletionReport.write(path, format=None)dispatcher.Time-series plots in
pyiwfm.visualization.plot_depletion:plot_cumulative_depletion(with optionalpumping_timeseriesoverlay),plot_depletion_timeseries,plot_depletion_summary_bar.Spatial maps in
pyiwfm.visualization.map_depletion:plot_depletion_map(reach-level color polylines),plot_stream_node_depletion_map(per-node scatter),plot_depletion_along_reach(longitudinal profile),export_depletion_geojsonandexport_stream_node_depletion_geojson.pyiwfm depletion <baseline> <scenario>CLI subcommand wires the above together:--output,--plot,--map,--node-level,--metric,--reach-ids/--node-ids,--sa-column,--crs.
Drawdown analysis suite (pyiwfm.io.drawdown,
pyiwfm.visualization.{plot,map}_drawdown, pyiwfm drawdown CLI)
DrawdownAtLocation/DrawdownTimeSeriesReport— drawdown vs time at a chosen set of(node, layer)locations.DrawdownSnapshot— per-node drawdown at a single timestep (kind="single") or per-node max across all timesteps (kind="max").DrawdownComputer.build_timeseries_report(locations, reference_timestep),build_snapshot(timestep, layer, reference_timestep),build_max_snapshot(layer, reference_timestep).Tabular writers:
write_drawdown_timeseries_csv/_json/_excelplusDrawdownTimeSeriesReport.write()dispatcher.Plots in
pyiwfm.visualization.plot_drawdown:plot_drawdown_timeseries,plot_drawdown_summary_bar.Maps in
pyiwfm.visualization.map_drawdown:plot_drawdown_map(cone-of-depression scatter),export_drawdown_geojson.pyiwfm drawdown <model_dir>CLI subcommand with--mode {timeseries,snapshot,max},--locations 1,1;42,2,--reference-timestep,--output,--plot,--no-map,--crs,--heads-hdf.
Documentation: editable inputs vs. computed outputs
New user guide page
docs/user_guide/inputs_vs_outputs.rstenumerates every model component (with its writer module and the Phase 2.1 mutation helper) versus every read-only analysis module (heads, hydrographs, subsidence, budgets, etc.).“Read-only by design” docstring annotations on
head_loader,hydrograph_loader,hydrograph_reader,area_loader,budget,zbudget,simulation_messages,drawdown,stream_depletion.
Structured exception handling for model loading (pyiwfm.core.exceptions,
pyiwfm.core.model)
New
ComponentLoadError(component_name, source_file, cause)— raised byIWFMModel.from_preprocessorandfrom_simulation_with_preprocessorwhen called with the newstrict=Truekwarg and a component fails to parse.IWFMModel.from_preprocessor(..., *, strict=False)andIWFMModel.from_simulation_with_preprocessor(..., *, strict=False)— passstrict=Truefrom calibration / analysis pipelines that require a complete model. The default lenient mode (strict=False) preserves the historical behavior: log a structured warning, record the error inmodel.metadata, and continue with whatever components loaded._COMPONENT_LOAD_EXCEPTIONStuple replaces the previousexcept Exceptioncatch-all at every component-load boundary incore/model.py(47 sites: 2 infrom_preprocessor, 35 infrom_simulation_with_preprocessor, plus the inner format- detection fallbacks). Programmer errors (TypeError,AttributeError,NameError,RuntimeError) now bubble up instead of being silently swallowed.
Centralized 1-based ID validation (pyiwfm.core.ids)
New
to_index(one_based_id, n_items, *, kind="id") -> intandto_indices(one_based_ids, n_items, *, kind="id") -> NDArray[int64]helpers replace inlineid - 1arithmetic. Both validate bounds and raiseValueErrorwith a self-documenting message that includeskind("element"/"node"/"reach") and the offending value(s).Adopted in
core/zones.py(4 sites) andcore/aggregation.py, closing a class of silent-overwrite bugs where invalid IDs would write past the end of preallocated arrays.
Frontend error containment (frontend/src/components/common/ErrorBoundary.tsx)
New React class component wraps each viewer tab body in
frontend/src/App.tsxso a render-time crash in one tab (Plotly, vtk.js, deck.gl) leaves the other tabs usable. Renders a fallback UI with a “Try again” button.
CI hardening (.github/workflows/ci.yml)
mypy src/pyiwfm/now runs across the full Python 3.10–3.14 × Ubuntu/Windows test matrix instead of only Python 3.12 / Ubuntu.New
docsjob runssphinx-build -W docs docs/_buildso documentation rot (broken refs, missing modules, malformed RST) fails CI instead of silently shipping.
Changed#
pyiwfm.io.stream_depletion._extract_stream_flownow requires an exact column-name match (default"Stream-Aquifer Interaction Within Model"). The previous substring matching against"gain from gw"/"stream-aquifer"and the(+)/(-)column sum-fallback were removed because they could silently misclassify columns. Older models that emit"Gain from GW (+)"should passsa_column="Gain from GW (+)"explicitly.
Fixed#
pyiwfm.io.gw_main_writer.write_gw_main_filenow coalesces the per-cellf.writeloop for aquifer parameters (and similarly for initial heads) into a single write per block. On a C2VSimFG-class model (~30k nodes × 4 layers) this collapses ~120k syscalls into one, makingsave_complete_modelsubstantially faster. Output is byte-identical to v1.1.x; locked in bytests/unit/test_gw_writer_vectorized.py.Same coalescing applied to
gw_subsidence_writer’s parametric grid block.pyiwfm.visualization.webapi.routes.export— fivejson.dumpscall sites (mesh GeoJSON export, model report, three records-export endpoints) now use a shared_json_defaultcallable that handlesnp.integer,np.floating(NaN → null),np.ndarray,pd.Timestamp, andbytes. Mesh GeoJSON exports no longer fail withTypeError: Object of type int64 is not JSON serializableon models with NumPyint64element indices.docs/user_guide/io.rst— example code usesmodel.mesh.n_nodesinstead of the aliasmodel.grid.n_nodes(meshis the canonical attribute per CLAUDE.md).CLAUDE.mdline 141 — corrected stale comment claimingwebapi/slicing.pyandwebapi/properties.pyare shims (they are 600-line full implementations imported byroutes/slices.pyand_mesh_state.pyrespectively).
[1.1.3] - 2026-04-23#
Added#
Python 3.14 support: CI now tests Ubuntu + Windows × 3.14, and
Programming Language :: Python :: 3.14added to the classifiers.
Fixed#
Tighten
[viz]and[webapi]extras tovtk>=9.6and (for[webapi])pyvista>=0.47. vtk 9.6.0 is the first version with Python 3.14 wheels, so the previousvtk>=9.0pin let pip backtrack through older vtk versions during resolution and emit a misleading ResolutionImpossible error in some user environments on Python 3.14. The new minimums keep pip on vtk versions that have 3.14 wheels and align with pyvista 0.47’s transitivevtk>=9.2.2,<9.7.0constraint.
[1.1.2] - 2026-04-23#
Added#
Stratigraphy Aquitard Accessors and Builder (pyiwfm.core.stratigraphy)
Stratigraphy.n_aquitardsproperty (equalsn_layers)Stratigraphy.get_aquitard_thickness(aquitard)— single aquitard thickness at every node (aquitard 0 = top aquitard above aquifer layer 0; aquitard k = between layer k-1 bottom and layer k top)Stratigraphy.get_all_aquitard_thicknesses()— vectorised(n_nodes, n_aquitards)arrayStratigraphy.get_node_aquitards(node_idx)— list of aquitard thicknesses at a specific node, mirroringget_node_elevationsStratigraphy.from_thicknesses(gs_elev, aquitard_thicknesses, aquifer_thicknesses, active_node=None)classmethod — construct a Stratigraphy directly from per-layer aquitard + aquifer thickness arrays (vectorised vianp.cumsum). Matches the IWFM file-format convention so users no longer need to hand-roll cumulative elevation math when programmatically generating stratigraphies with aquitards.The stratigraphy writer and
read_stratigraphynow both delegate the aquitard math to these helpers so it has a single owner.
AquiferParameters Dispatcher (pyiwfm.components.groundwater)
AquiferParameters.get_array(param)/.get_layer(param, layer)/.get_at(param, node_idx, layer)— unified accessors keyed by short parameter names ("kh","kv","ss","sy","aquitard_kv"); unknown names raiseKeyError, unset arrays raiseValueErrorExisting
get_layer_kh/get_layer_kvare now thin shims over the dispatcher (no breaking change)
Stream Reach → Groundwater Node Mapping (pyiwfm.components.stream)
AppStream.get_gw_nodes_in_reach(reach_id)returnslist[int | None]of gw node IDs in upstream-to-downstream order
Element Centroid Cache (pyiwfm.core.mesh)
AppGrid.element_centroidslazy property — vectorised(n_elements, 2)array built in a single pass, cached until mesh mutation invalidatesAppGrid.get_element_centroid(element_id)now an O(1) dict + array lookup after first call (previously re-computed per call)
Observation Upload Format Expansion
Web viewer observation upload now supports CSV, TSV, SMP, and generic whitespace-delimited text files (previously CSV only)
Automatic delimiter detection (comma, tab, whitespace) from file content
Content-based SMP format sniffing — SMP data is recognized regardless of file extension (
.txt,.dat,.smp, etc.)Frontend upload wizard auto-detects format and skips column mapping for SMP
File extensions
.csv,.tsv,.txt,.dat,.smpall acceptedNormalized upload API response shape for consistent frontend handling
Calibration Enhancements
compute_composite_continuous(): T-weighted composite heads at all simulation timesteps (full temporal resolution for CalcTypHyd)average_to_seasonal(): Aggregate continuous monthly series to seasonal window averages for sim/obs comparisoncompute_obs_type_hydrographs(): Convenience wrapper for observed type hydrograph computation with PEST outputBIANNUAL_SEASONS: Spring/Fall seasonal period definitions for clustering
Changed#
Budget plot colors: improved contrast for Ice Blue and Cream palette entries
Budget table: colored squares before component names, adjusted column widths, moved discrepancy percentage to its own row
Simulation Reader Refactor
IWFMSimulationReader.read()split into focused section methods (_read_titles,_read_input_files,_read_time_settings,_read_processing_options,_read_solver_settings,_read_convergence_tail) for easier piecewise testing. No behavior change; 11 fixed input-file slots now driven by the_INPUT_FILE_FIELDStable.
Test & Dev-Env Improvements
[dev]extra now pulls in[mesh]sopip install -e ".[dev]"yields a full test environment (mesh-wrapper unit tests no longer silently skip); triangle pin relaxed to>=20200424with a Python 3.14 compat marker.Graceful module-/class-level
importorskipguards forhypothesisproperty-test files andpytest-benchmark-dependent classes.tests/unit/test_results_extraction_subsidence.pyresolves the C2VSimFG subsidence HDF5 viaC2VSIMFG_SUBS_HDForC2VSIMFG_DIRenv vars (previously hard-coded) so CI and contributors can unskip those 3 tests by configuring the existing data location.New integration test
tests/integration/test_stratigraphy_roundtrip.pyexercises aquitard helpers and read → write → read against the IWFM Sample Model and C2VSimFG.
Fixed#
Release workflow now uses
python -m buildinstead ofhatch build, so the wheel is built from the sdist (isolated) rather than from the source tree. This prevents the sdist-then-wheel version-mismatch that affected v1.1.0 and v1.1.1, wherehatch_build.py’snpm run buildcould dirty tracked files between packaging steps and cause hatch-vcs to bump the wheel to the next dev version. The workflow also pins the version from the git tag viaSETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYIWFMand asserts sdist/wheel filenames match before uploading, as a safety net.pyiwfm.io.ascii.read_stratigraphynow correctly handles stratigraphy files whose node IDs are not 1-based contiguous. The previous dead-codeidx = node_id - 1branch silently scrambled row assignments; now builds anode_id → rowmapping from the file and raisesFileFormatErroron duplicate IDs.DSSFile.__init__validates themodeargument (raisesValueErrorfor anything other than"r"/"w"/"rw") and the three deadpass-only mode-dispatch branches inopen()have been removed — HEC-DSS 7’szopenExtendeddoes not expose per-mode open semantics, so the branches had no effect.pyiwfm.io.supply_adjustnow importsCOMMENT_CHARSandstrip_inline_commentfrompyiwfm.io.iwfm_readerinstead of duplicating them locally (per CLAUDE.md’s “never duplicate these helpers” rule). Blank-line-as-data semantics for the DSSFL slot are preserved via a small local_is_fortran_commentwrapper.
[1.1.0] - 2026-03-13#
Drawdown, Diagnostics, Stream Depletion, Mesh Quality, Budget Checks, and PEST++ CLI
Added#
Drawdown Analysis (pyiwfm.io.drawdown)
DrawdownComputer: Compute per-node and per-element drawdown relative to a reference timestepcompute_drawdown_range(): Robust min/max (2nd–98th percentile) for consistent color scaling across animation framescompute_max_drawdown_map(): Maximum drawdown at each node across all timestepsWeb viewer: Color By: Drawdown mode in Results Map with diverging color scale and reference timestep slider
Stream Depletion (pyiwfm.io.stream_depletion)
compute_stream_depletion(): Compare baseline and pumping-scenario model runs to quantify per-reach stream flow depletionStreamDepletionResult: Per-reach timeseries (baseline, scenario, depletion, cumulative, max, total)StreamDepletionReport: Aggregate report across multiple reaches
Budget Checks (pyiwfm.io.budget_checks)
check_budget_balance(): Per-location mass balance sanity check with configurable tolerancecheck_all_budgets(): Check mass balance across all available budget typesBudgetSanityReport: Summary of violations, max/mean percent error
Mesh Quality (pyiwfm.core.mesh_quality)
compute_mesh_quality(): Element-level quality metrics (aspect ratio, skewness, min/max angle, area) with aggregate statisticsMeshQualityReport: Summary with poor-quality element count and serialization to dict for API responsesWeb viewer: Mesh quality card on Overview tab
Simulation Diagnostics (pyiwfm.visualization.webapi.routes.diagnostics)
/api/diagnostics/messages: Paginated messages with severity filtering/api/diagnostics/convergence: Iteration count timeseries/api/diagnostics/mass-balance: Per-component mass balance error/api/diagnostics/summary: Combined diagnostics summary/api/diagnostics/spatial-summary: Spatial message counts for map overlayWeb viewer: Diagnostics tab with interactive charts and message table
Unsaturated Zone Routes (pyiwfm.visualization.webapi.routes.unsaturated_zone)
/api/unsaturated-zone/: Component info endpoint/api/unsaturated-zone/summary: Parameter summary endpoint
PEST++ CLI (pyiwfm.cli.pest)
pyiwfm pest setup: Generate PEST++ control, template, and instruction files from an IWFM model directorypyiwfm pest run: Execute PEST++ with configurable executable and workerspyiwfm pest analyze: Post-process results in text or JSON format
Web Viewer Enhancements
Subsidence surface visualization on Results Map
Model comparison dialog with mesh/stratigraphy diff
HTML/JSON report export endpoints
Small watersheds and unsaturated zone API routes
Documentation
New tutorials: Drawdown Analysis, Stream Depletion, Simulation Diagnostics, Mesh Quality
New gallery pages: Mesh Quality, Drawdown Maps, Convergence
Updated API reference with new modules
Updated web viewer guide with 6-tab layout and new features
PEST++ CLI section in user guide
Diagnostics user guide
[1.0.4] - 2026-03-04#
IWFM2OBS Timestamp Alignment and CalcTypHyd Fortran-Matching Features
Added#
IWFM2OBS Timestamp Alignment (pyiwfm.calibration.iwfm2obs)
_compute_model_dates(): Replicate FortranComputeDatefor 1MON/1DAY/1WEEK/1YEAR_replace_timestamps(): Replace parsed.outtimestamps with computed datesexpand_obs_to_layers(): Auto-expand base observation IDs to per-layer variantsdeduplicate_smp(): Strip%Nlayer suffixes from SMP filesCLI:
--deduplicate-smpmodeVerified: max
|diff|= 0.000060 ft vs Fortran on C2VSimFG (48,816 GW bore IDs)
CalcTypHyd Fortran-Matching Features (pyiwfm.calibration.calctyphyd)
read_calctyphyd_config()/CalcTypHydFileConfig: Parse Fortran.inconfig filescompute_typical_hydrographs_timeseries(): Per-period-year output matching Fortran algorithm (period-year slot averaging, mean from slot averages)write_pest_output(): Write PEST.out/.insfiles with exact Fortran column formatread_cluster_weights(n_clusters=): Header detection and column limitingCalcTypHydConfig.start_date/end_date: Date-range filteringCLI:
--configmode for Fortran.infiles,--output-dirfor PEST filesVerified: byte-identical output vs Fortran on C2VSimFG (6 clusters × 62 entries)
[1.0.2] - 2026-02-27#
IWFM2OBS Model Discovery and Multi-Layer Output
Added#
Model File Discovery (pyiwfm.calibration.model_file_discovery)
discover_hydrograph_files(): Parse IWFM simulation main file to auto-discover GW and stream hydrograph.outfile paths and hydrograph metadata (bore IDs, layers, coordinates)HydrographFileInfo: Dataclass with discovered paths, hydrograph locations, start date, and time unit
Observation Well Specification (pyiwfm.calibration.obs_well_spec)
read_obs_well_spec(): Read observation well specification files for multi-layer target processing (name, coordinates, element, screen top/bottom)ObsWellSpec: Dataclass for multi-layer well screen geometry
IWFM2OBS Model-Discovery Mode (pyiwfm.calibration.iwfm2obs)
iwfm2obs_from_model(): Full IWFM2OBS workflow that reads.outfiles directly via simulation main file discovery — combines the old Fortran IWFM2OBS’s direct file reading with the new multi-layer T-weighted averagingIWFM2OBSConfig: Configuration dataclass for the integrated workflowwrite_multilayer_output(): WriteGW_MultiLayer.outformat (Name, Date, Time, Simulated, T1-T4, NewTOS, NewBOS)write_multilayer_pest_ins(): Write PEST instruction file withWLT{well:05d}_{timestep:05d}naming at columns 50:60
Hydrograph Reader Enhancement (pyiwfm.io.hydrograph_reader)
IWFMHydrographReader.get_columns_as_smp_dict(): Extract.outcolumns asSMPTimeSeriesdict, bridging the hydrograph reader to the interpolation pipeline
CLI Model-Discovery Mode (pyiwfm.cli.iwfm2obs)
--modelflag for automatic model file discovery from simulation main file--obs-gw,--output-gw,--obs-stream,--output-streamfor per-type observation/output SMP paths--well-spec,--multilayer-out,--multilayer-insfor multi-layer processing
[1.0.0] - 2026-02-24#
Calibration Tools, Clustering, and Publication-Quality Plotting
Added#
SMP Observation File I/O (pyiwfm.io.smp)
SMPReader/SMPWriter: Read and write IWFM SMP (Sample/Bore) observation filesSMPRecord,SMPTimeSeries: Typed containers for bore ID, datetime, value, exclusion flagFixed-width parsing with sentinel value (NaN) handling
SimulationMessages.out Parser (pyiwfm.io.simulation_messages)
SimulationMessagesReader: Parse IWFM simulation diagnostic output filesSimulationMessage: Structured message with severity, procedure, spatial IDsRegex-based extraction of node, element, reach, and layer IDs
to_geodataframe(): Map messages to spatial locations for GIS analysis
IWFM2OBS Time Interpolation (pyiwfm.calibration.iwfm2obs)
interpolate_to_obs_times(): Linear/nearest interpolation of simulated to observed timescompute_multilayer_weights(): Transmissivity-weighted averaging for multi-layer wellscompute_composite_head(): Composite head from layer heads using T-weightsiwfm2obs(): Complete workflow function matching Fortran IWFM2OBS utility
Typical Hydrograph Computation (pyiwfm.calibration.calctyphyd)
compute_typical_hydrographs(): Seasonal averaging + de-meaning + weighted combinationcompute_seasonal_averages(): Per-well seasonal period averagingread_cluster_weights(): Parse cluster weight files for CalcTypHyd input
Fuzzy C-Means Clustering (pyiwfm.calibration.clustering)
fuzzy_cmeans_cluster(): NumPy-only fuzzy c-means with spatial + temporal featuresClusteringResult: Membership matrix, cluster centers, fuzzy partition coefficientto_weights_file(): Export weights in CalcTypHyd-compatible formatFeature extraction: cross-correlation, amplitude, trend, seasonal strength
Calibration Plots (pyiwfm.visualization.calibration_plots)
plot_calibration_summary(): Multi-panel publication figure (1:1, spatial bias, histogram, metrics)plot_hydrograph_panel(): Grid of observed vs simulated hydrographsplot_metrics_table(): Matplotlib table of per-well statisticsplot_residual_histogram(): Residual distribution with optional normal fitplot_water_budget_summary()/plot_zbudget_summary(): Stacked bar budget chartsplot_cluster_map(): Spatial cluster membership visualizationplot_typical_hydrographs(): Overlay of typical hydrographs by cluster
New Plot Functions (pyiwfm.visualization.plotting)
plot_one_to_one(): Scatter with 1:1 line, regression, and metrics text boxplot_spatial_bias(): Diverging colormap of observation bias on mesh background
Publication Matplotlib Style (visualization/styles/pyiwfm-publication.mplstyle)
Journal-quality defaults: serif fonts, no top/right spines, 300 DPI, constrained layout
Scaled RMSE Metric (pyiwfm.comparison.metrics)
scaled_rmse(): Dimensionless RMSE / (max - min) for cross-site comparisonAdded
scaled_rmsefield toComparisonMetricsdataclass
CLI Subcommands
pyiwfm iwfm2obs: Time interpolation of simulated to observed SMP timespyiwfm calctyphyd: Compute typical hydrographs from clustered observation wells
[0.4.0] - 2026-01-15#
Supplemental Package Support and Web Viewer Enhancements
Fixed#
Root Zone Version-Dependent Parsing Bugs
Fixed ARSCLFL (land use area scaling) version guard: only read for v4.12+, not v4.11+
Fixed FinalMoistureOutFile (FMFL) read: skip for v4.12+ where it was removed
Fixed root zone soil parameter table parsing when
n_elementsis known
Changed#
I/O Reader Deduplication (pyiwfm.io.iwfm_reader)
Centralized
resolve_path(),next_data_or_empty(),parse_version(), andversion_ge()intoiwfm_reader.py— the canonical module for all IWFM file-reading utilitiesReplaced 14 identical
_next_data_or_emptymethod copies across reader modules with thin wrappers delegating to the central functionReplaced 11 identical
_resolve_pathcopies (10 methods + 1 module-level function inpreprocessor.py) with delegations toiwfm_reader.resolve_path()Unified version parsing:
rootzone.parse_versionandstreams.parse_stream_versionmerged intoiwfm_reader.parse_version()(handles both.and-separators)Net reduction of ~42 lines across 16 files with no behavior changes
Added#
Small Watershed Component (pyiwfm.components.small_watershed)
AppSmallWatershed: Container for small watershed model unitsWatershedUnit: Individual watershed with root zone and aquifer parametersWatershedGWNode: Groundwater node connection with percolation ratesfrom_config(): Build component from reader configvalidate(): Check areas, stream node references, GW node connectivity
Unsaturated Zone Component (pyiwfm.components.unsaturated_zone)
AppUnsatZone: Container for unsaturated zone elementsUnsatZoneElement: Per-element layer data with initial soil moistureUnsatZoneLayer: Layer-level soil hydraulic propertiesfrom_config(): Build component from reader configvalidate(): Check layer counts and element consistency
Small Watershed Writer (pyiwfm.io.small_watershed_writer)
SmallWatershedComponentWriter: Template-based writer for small watershed filesSmallWatershedWriterConfig: Writer configuration with output pathsJinja2 template for IWFM v4.0 format with geospatial, root zone, and aquifer sections
Unsaturated Zone Writer (pyiwfm.io.unsaturated_zone_writer)
UnsatZoneComponentWriter: Template-based writer for unsaturated zone filesUnsatZoneWriterConfig: Writer configuration with output pathsJinja2 template for IWFM v4.0 format with element data and initial moisture sections
Model Integration
IWFMModel.small_watersheds: Optional small watershed component attributeIWFMModel.unsaturated_zone: Optional unsaturated zone component attributeCompleteModelWriter: Dedicated writers replace passthrough file copy for both packagesFull roundtrip support for small watershed and unsaturated zone files
BaseComponent ABC (pyiwfm.core.base_component)
BaseComponent: Abstract base class withvalidate()andn_itemsinterfaceAll 6 components (AppGW, AppStream, AppLake, RootZone, AppSmallWatershed, AppUnsatZone) inherit from
BaseComponent
Model Factory Extraction (pyiwfm.core.model_factory)
Extracted 6 helper functions (~420 lines) from
IWFMModelto reduce the God Object patternPublic functions:
build_reaches_from_node_reach_ids,apply_kh_anomalies,apply_parametric_grids,apply_parametric_subsidence,binary_data_to_model,resolve_stream_node_coordinatesIWFMModelclassmethods delegate to factory functions (backward compatible)
Writer Config Consolidation (pyiwfm.io.writer_config_base)
BaseComponentWriterConfig: Shared base dataclass for all 6 component writer configs (common fields: output_dir, version, length/volume factors/units, subdir)
I/O Reader Deduplication
Consolidated
_resolve_path()and_parse_version()duplicates across readers to use canonical implementations fromiwfm_reader.pyConsolidated Fortran binary record I/O between
base.pyandbinary.py
Web Viewer Performance Improvements
Cached
node_id_to_idxandelem_id_to_idxmappings inModelStateinstead of rebuilding per requestCached hydrograph location data (GW and stream) in
ModelStateDrawdown endpoint now supports pagination:
offset,limit,skipparameters for frame-by-frame animation playback
New Web Viewer API Endpoints
GET /api/export/geopackage: Download multi-layer GeoPackage (nodes, elements, streams, subregions, boundary) viaGISExporterGET /api/export/plot/{plot_type}: Generate publication-quality matplotlib figures (mesh, elements, streams, heads) as PNG or SVGPOST /api/model/compare: Load a second model and compare meshes/stratigraphy viaModelDifferGET /api/results/statistics: Time-aggregated head statistics (min, max, mean, std per node across all timesteps)
FastAPI Web Viewer Enhancements (2026-02)
Results Map tab with deck.gl + MapLibre GL for head contour visualization
Budgets tab with Plotly charts for GW, stream, and other budget types
Hydrograph overlay with observed vs simulated data
Server-side coordinate reprojection via pyproj (model CRS to WGS84)
Stream node z elevation from stratigraphy ground surface
Monthly budget timestep support using
relativedeltaBudget units populated from column type codes
CRS default corrected to proj string for C2VSimFG
Component Exports
Top-level
pyiwfmpackage now exports AppGW, AppStream, AppLake, RootZone, AppSmallWatershed, AppUnsatZone for convenientfrom pyiwfm import AppGW
Budget & Zone Budget Excel Export (pyiwfm.io.budget_excel, pyiwfm.io.zbudget_excel)
budget_to_excel(): Export budget HDF5 data to formatted Excel workbooks (one sheet per location, bold titles/headers, auto-fit columns)zbudget_to_excel(): Same for zone budget data (one sheet per zone)budget_control_to_excel()/zbudget_control_to_excel(): Batch export from control file configuration (one.xlsxper budget spec)Unit conversion factors (FACTLTOU/FACTAROU/FACTVLOU) applied per column type using codes from
Budget_Parameters.f90
Budget & Zone Budget Control File Parsers (pyiwfm.io.budget_control, pyiwfm.io.zbudget_control)
read_budget_control(): Parse IWFM budget post-processor control files (FACTLTOU, UNITLTOU, FACTAROU, UNITAROU, FACTVLOU, UNITVLOU, dates, per-budget HDF5 paths, output paths, location IDs)read_zbudget_control(): Parse IWFM zone budget control files (same pattern with zone definition file support)BudgetControlConfig/ZBudgetControlConfig: Typed dataclasses
Budget Utilities (pyiwfm.io.budget_utils)
apply_unit_conversion(): Apply IWFM conversion factors per column typeformat_title_lines(): Substitute@UNITVL@,@UNITAR@,@LOCNAME@,@AREA@markers in title templatesfilter_time_range(): Filter DataFrames by IWFM date range
Budget CLI Commands (pyiwfm.cli.budget, pyiwfm.cli.zbudget)
pyiwfm budget <control_file>: Export budgets to Excel from control filepyiwfm zbudget <control_file>: Export zone budgets to Excel from control file--output-dirflag to override output directory
Budget Unit Conversion in Readers
BudgetReader.get_dataframe()now accepts keyword-onlylength_factor,area_factor,volume_factorfor on-the-fly unit conversionZBudgetReader.get_dataframe()now accepts keyword-onlyvolume_factorBackward compatible: all factors default to 1.0
Budget Excel Download Endpoints
GET /api/budgets/{budget_type}/excel: Download formatted budget workbookGET /api/export/budget-excel: Download budget Excel from the export routes
Documentation
Added ~30 missing module entries to API docs (
docs/api/io.rst)Added Small Watershed and Unsaturated Zone to component docs
Reorganized I/O docs into logical sections (Core, GW, Stream, Lake, RZ, Supplemental, etc.)
Added BaseComponent and model_factory to core API docs
Added writer_config_base to I/O API docs
Added API routes summary to visualization docs
[0.2.0] - 2025-06-01#
Complete IWFM File Writers Implementation
Added#
IWFMModel Class Methods
IWFMModel.from_preprocessor(pp_file): Load from preprocessor input files (mesh, stratigraphy, geometry)IWFMModel.from_preprocessor_binary(binary_file): Load from native IWFM preprocessor binary (ACCESS='STREAM')IWFMModel.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 filemodel.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 formatmodel.gridproperty: Alias formeshfor compatibility
Complete Model I/O
pyiwfm.io.load_complete_model: Load complete IWFM model from simulation main filepyiwfm.io.save_complete_model: Save complete IWFM model to all input filesFull roundtrip support for reading, modifying, and writing IWFM models
Time Series ASCII I/O
pyiwfm.io.timeseries_ascii: ASCII time series reader/writer moduleTimeSeriesWriter: Write IWFM ASCII time series files with 21-char timestamp formatTimeSeriesReader: Read IWFM ASCII time series filesformat_iwfm_timestamp: Format datetime to IWFM 21-character timestampparse_iwfm_timestamp: Parse IWFM timestamp string to datetime
Groundwater Component I/O
pyiwfm.io.groundwater: Complete groundwater component file I/OGroundwaterWriter: Write wells, pumping, boundary conditions, aquifer parametersGroundwaterReader: Read groundwater component filesGWFileConfig: Configuration for groundwater file paths
Stream Component I/O
pyiwfm.io.streams: Complete stream network component file I/OStreamWriter: Write stream nodes, reaches, diversions, bypasses, rating curvesStreamReader: Read stream component filesStreamFileConfig: Configuration for stream file paths
Lake Component I/O
pyiwfm.io.lakes: Complete lake component file I/OLakeWriter: Write lake definitions, elements, rating curves, outflowsLakeReader: Read lake component filesLakeFileConfig: Configuration for lake file paths
Root Zone Component I/O
pyiwfm.io.rootzone: Complete root zone component file I/ORootZoneWriter: Write crop types, soil parameters, land useRootZoneReader: Read root zone component filesRootZoneFileConfig: Configuration for root zone file paths
Simulation Control I/O
pyiwfm.io.simulation: Simulation control file I/OSimulationWriter: Write simulation main control fileSimulationReader: Read simulation control fileSimulationConfig: Simulation configuration dataclass
HEC-DSS 7 Support
pyiwfm.io.dss: Complete HEC-DSS 7 time series I/O packageDSSFile: Context manager for DSS file operations using ctypesDSSPathname: DSS pathname representation (/A/B/C/D/E/F/)DSSPathnameTemplate: Template for generating pathnamesDSSTimeSeriesWriter: High-level time series writerDSSTimeSeriesReader: High-level time series readerwrite_timeseries_to_dss: Convenience function for single time seriesread_timeseries_from_dss: Convenience function for reading time serieswrite_collection_to_dss: Write TimeSeriesCollection to DSSHAS_DSS_LIBRARY: Flag indicating DSS library availability
Template Engine Updates
iwfm_timestampfilter: Format datetime for IWFM filesdss_pathnamefilter: Format DSS pathnamestimeseries_reffilter: Reference time series filesiwfm_array_rowfilter: Format array rows for IWFM files
Changed#
Updated
pyiwfm.io.__init__.pywith all new exportsExtended
pyiwfm.io.preprocessorwithload_complete_model()andsave_complete_model()
[0.3.0] - 2025-10-01#
PEST++ Calibration Interface, Multi-Scale Viewing, and Subprocess Runner
Added#
Subprocess Runner (pyiwfm.runner)
IWFMRunner: Run IWFM executables (Preprocessor, Simulation, Budget, ZBudget) via subprocessIWFMExecutables: Locate and manage IWFM executable pathsfind_iwfm_executables(): Auto-detect executables in PATH or specified directoriesRunResult,PreprocessorResult,SimulationResult,BudgetResult,ZBudgetResult: Typed result classes
Scenario Management (pyiwfm.runner.scenario)
Scenario: Define named model scenarios with parameter overridesScenarioManager: Manage, run, and compare multiple scenariosScenarioResult: Collect and compare scenario outputs
PEST++ Integration (pyiwfm.runner.pest)
PESTInterface: Low-level PEST++ control file interfaceTemplateFile,InstructionFile: PEST++ template/instruction file managementObservationGroup: Observation group definitionswrite_pest_control_file(): Generate PEST++ control files (.pst)
PEST++ Parameter Management (pyiwfm.runner.pest_params)
IWFMParameterType: Enum of all IWFM parameter types (aquifer, stream, lake, rootzone, flux multipliers)ParameterTransform: Log/none/tied/fixed transform typesParameterGroup,Parameter: Parameter definitions with bounds and transformsParameterization strategies:
ZoneParameterization,MultiplierParameterization,PilotPointParameterization,DirectParameterization,StreamParameterization,RootZoneParameterizationIWFMParameterManager: Central parameter registry and management
PEST++ Observation Management (pyiwfm.runner.pest_observations)
IWFMObservationType: Enum of observation types (head, drawdown, flow, stage, etc.)IWFMObservation,IWFMObservationGroup: Observation definitions with weightsObservationLocation: Spatial location for observationsWeightStrategy: Equal, inverse variance, decay, and group contribution weightingDerivedObservation: Computed observations (gradients, differences)IWFMObservationManager: Central observation registryWellInfo,GageInfo: Monitoring point metadata
PEST++ Template/Instruction Generation (pyiwfm.runner.pest_templates, pest_instructions)
IWFMTemplateManager: Generate PEST++ template files (.tpl) from IWFM input filesTemplateMarker: Define parameter marker locations in templatesIWFMFileSection: Track file sections for template generationIWFMInstructionManager: Generate PEST++ instruction files (.ins) from IWFM outputOutputFileFormat: Define output file parsing rulesIWFM_OUTPUT_FORMATS: Predefined formats for standard IWFM outputs
Geostatistics (pyiwfm.runner.pest_geostat)
VariogramType: Spherical, exponential, Gaussian, power variogram modelsVariogram: Variogram definition with nugget, sill, range parametersGeostatManager: Manage spatial correlation structures and pilot point krigingcompute_empirical_variogram(): Compute empirical variograms from spatial data
Main PEST++ Helper Interface (pyiwfm.runner.pest_helper)
IWFMPestHelper: High-level interface coordinating all PEST++ componentsConvenience methods:
add_zone_parameters(),add_multiplier(),add_pilot_points(),add_stream_parameters(),add_rootzone_parameters()add_head_observations(),add_streamflow_observations()set_svd(),set_regularization(),set_model_command(),set_pestpp_options()build(): Generate complete PEST++ setup (control file, templates, instructions, scripts)run_pestpp(): Execute PEST++ from within PythonSVDConfig,RegularizationConfig,RegularizationType: Configuration classes
Ensemble Management (pyiwfm.runner.pest_ensemble)
IWFMEnsembleManager: Prior/posterior ensemble generation for pestpp-iesEnsembleStatistics: Statistical analysis of parameter ensemblesLatin Hypercube Sampling and geostatistical realization generation
CSV I/O compatible with PEST++ ensemble format
Uncertainty reduction analysis between prior and posterior ensembles
Post-Processing (pyiwfm.runner.pest_postprocessor)
PestPostProcessor: Load and analyze PEST++ output files (.rei, .sen, .iobj, .par)CalibrationResults: Container for all calibration output dataResidualData: Observation residual analysis with weighted statistics and group phiSensitivityData: Parameter sensitivity rankingsFit statistics: RMSE, MAE, R-squared, Nash-Sutcliffe efficiency, bias, percent bias
Parameter identifiability analysis
Summary report generation
Export to CSV and PEST format
Zone Management (pyiwfm.core.zones)
Zone: Dataclass for zone definition with id, name, elements, areaZoneDefinition: Element-to-zone mapping with query and validationFactory methods:
from_subregions(),from_element_list()Zone CRUD operations (add, remove, rename)
Data Aggregation (pyiwfm.core.aggregation)
DataAggregator: Spatial aggregation engine with 6 methodsAggregationMethod: Enum (sum, mean, area_weighted_mean, min, max, median)aggregate_to_array(): Expand zone values back to elements for visualizationaggregate_timeseries(): Multi-timestep aggregationcreate_aggregator_from_grid(): Factory from AppGrid
Model Query API (pyiwfm.core.query)
ModelQueryAPI: High-level unified query interface for multi-scale data accessget_values(): Fetch data at any spatial scale with configurable aggregationget_timeseries(): Retrieve temporal data for locations or zonesexport_to_dataframe(),export_to_csv(): Data export to Pandas and CSVDynamic variable and scale discovery
Zone File I/O (pyiwfm.io.zones)
read_iwfm_zone_file(),write_iwfm_zone_file(): IWFM ZBudget zone formatread_geojson_zones(),write_geojson_zones(): GeoJSON with geometryread_zone_file(),write_zone_file(): Auto-detecting universal I/Oauto_detect_zone_file(): Format detection by extension and content
Interactive Web Viewer (pyiwfm.visualization.webapi)
FastAPI backend + React SPA frontend with 4 tabs (Overview, 3D Mesh, Results Map, Budgets)
ModelStatesingleton for lazy model and results loadingvtk.js-based 3D mesh rendering with layer visibility, cross-section slicing, and z-exaggeration
deck.gl + MapLibre Results Map with head contours and hydrograph markers
Plotly budget charts with location/column selection
Stream network overlay on both 3D and 2D views
Server-side coordinate reprojection via
pyprojpyiwfm viewerCLI launcher with--model-dir,--crs,--portoptionsAuto-detection of preprocessor and simulation files
Graceful degradation for missing components
[0.1.0] - 2024-07-01#
Initial release of pyiwfm.
Added#
Core Modules
pyiwfm.core.mesh: AppGrid, Node, Element, Face classes for mesh representationpyiwfm.core.stratigraphy: Stratigraphy class for layer structurepyiwfm.core.timeseries: TimeSeries and TimeSeriesCollection classes
Component Modules
pyiwfm.components.groundwater: AppGW, AquiferParameters, Well classespyiwfm.components.stream: AppStream, StrmNode, StrmReach classespyiwfm.components.lake: AppLake, Lake, LakeElement classespyiwfm.components.rootzone: RootZone, LandUse, Crop classespyiwfm.components.connectors: StreamGWConnector, LakeGWConnector
I/O Modules
pyiwfm.io.ascii: ASCII file readers and writerspyiwfm.io.binary: Fortran binary file handlerspyiwfm.io.hdf5: HDF5 file handlers
Mesh Generation
pyiwfm.mesh_generation.generators: MeshGenerator ABC and MeshResultpyiwfm.mesh_generation.constraints: BoundaryConstraint, LineConstraint, PointConstraint, RefinementZonepyiwfm.mesh_generation.triangle_wrapper: TriangleMeshGeneratorpyiwfm.mesh_generation.gmsh_wrapper: GmshMeshGenerator
Visualization
pyiwfm.visualization.gis_export: GISExporter for GeoPackage, Shapefile, GeoJSONpyiwfm.visualization.vtk_export: VTKExporter for 2D and 3D VTK filespyiwfm.visualization.plotting: matplotlib-based visualization functions
Comparison
pyiwfm.comparison.differ: ModelDiffer, MeshDiff, StratigraphyDiffpyiwfm.comparison.metrics: ComparisonMetrics, TimeSeriesComparison, SpatialComparisonpyiwfm.comparison.report: ReportGenerator with text, JSON, and HTML output
Documentation#
Comprehensive Sphinx documentation with PyData theme
User guide with installation, quickstart, and detailed guides
Tutorials for mesh generation, visualization, and model comparison
Full API reference with examples