Skip to content

Add the optional MODFLOW groundwater coupling - #357

Open
soaressgabriel wants to merge 10 commits into
mainfrom
feat/356-modflow-coupling
Open

soaressgabriel wants to merge 10 commits into
mainfrom
feat/356-modflow-coupling

Conversation

@soaressgabriel

@soaressgabriel soaressgabriel commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Checklist

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?
  • Added tests for changed code.
  • Updated documentation for changed code.

Description

Optional coupling of RUBEM with MODFLOW-2005 through the PCRaster MODFLOW extension (pcraster.initialise and the mf2005 executable, both shipped by the conda-forge pcraster package on linux-64 and win-64). The first commit is LINA OSORIO's prototype (feat_modflow_module / feat_calibrator_modflow, commits ed24a30..da41faa) ported as she wrote it, with her authorship; the next commits rework it by concern. Without a MODFLOW section nothing changes: the default path is proven byte-identical by a test that runs the synthetic dataset with the section absent, present-but-disabled and {"enabled": false} and compares every raster and CSV, and by the exact job.

  • Configuration (rubem/configuration/modflow_configuration.py, both file formats, rubem config schema and rubem config migrate): section MODFLOW (legacy) / modflow (1.0), extra="forbid" in both. Layers are listed from the top down and numbered like MODFLOW (layer 1 = top); the code converts to the extension's bottom-up numbers (ModflowSettings.pcraster_layer). Each layer has its own bottom, initial_head, boundary, laytype, horizontal_conductivity (map, number or {map, table} class lookup), vertical_conductivity, specific_storage, specific_yield; a model top map; dis, solver (PCG), wetting, river/ghb/drain with entries that list their target layers, coupling.dynamic_root_depth, output. Rules enforced by the model: river.enabled when the section is enabled, LAYCON 1 only on the top layer, wetting only on LAYCON 1/3 layers, storage keys per LAYCON in transient runs, every layer reference in range and once per package, at most 9 layers (the mfh<n> output names), output.storage only in transient runs, numeric river conductance needs mask; every violation is reported at once. A present but disabled section yields the non-blocking "MODFLOW section is ignored." problem.
  • Validation (rubem/validation/modflow_inputs.py, fourth tier of rubem/_deps.py): always, the files the run reads exist and the runtime is available (pcraster._pcraster_modflow importable, mf2005 on PATH or under the interpreter prefix, prepended to PATH for the extension and the calibration workers), both blocking even with -s; with validation on, the clone geometry of every map, boundary values in {-1, 0, 1}, top above the bottoms and bottoms decreasing downward on active columns, finite heads/conductivities/storage on active cells, heads below the layer bottom (blocking when every active cell, warning with the count otherwise), kh tables covering the classes of the active cells with positive values, at least one cell per enabled package layer (the extension ends the process on a getter for an empty package), the riverbed-versus-layer diagnostic ("N of M river cells of layer k have their bed outside the layer's elevation interval: B below the layer bottom, A above the layer top", non-blocking), and the Dpz_min table rules.
  • Groundwater module (rubem/hydrological_processes/_modflow.py, ModflowGroundwater): inputs read once with the repository readers; DIS -> BAS -> BCF -> wetting -> solver, stress packages set once (the extension keeps them across periods); one RUBEM step = one stress period of the days of the month (updateDISParameter); recharge in mm/step to m/day, RCH option 3; mf.run(<output>/modflow) so nothing lands in the working directory; getters only for package layers with cells; non-convergence raises RuntimeError naming the period and pcrmf.lst; heads, storage, drain flow and river exchange keyed by the user's layer numbers. The extension records LAYCON by call order, so BCF is set bottom-up (proved with the real mf2005 on a three-layer grid).
  • Dynamic model: when enabled, bfw is the aquifer-to-river RIV leakage in mm (the saturated-zone reservoir is no longer updated), diagnostics go through the standard raster writer (mfh<n>, mfst<n>, mfdrn<n>, mfaq2rv, mfrv2aq, mfrvnet, mfwt, mfgwd, mfzr, mfzfrac), so the formats apply and the calibrator's raster-free evaluation writes none; the run directory <output>/modflow is created at initialization (refused when it already exists) and removed after the last step, kept when a step raised. The optional root-depth coupling (prototype formulation, one-step lag, vegetation water stress on the root fraction) is off by default.
  • Calibrator: the section survives the evaluation document (legacy and 1.0 round trip), so every evaluation runs the coupled model. MODFLOW parameters enter the search only when named in --bound/--fix: modflow.layers.<n>.specific_yield, modflow.layers.<n>.specific_storage (only the storage the layer's LAYCON reads, transient runs), modflow.layers.<n>.kh.<class> (numeric rows of the layer's lookup table), modflow.river.<i>.conductance (numeric entries); bounds are mandatory, finite, positive, specific_yield within (0, 1]. Per-evaluation kh tables are written in the evaluation directory; the calibrated configuration and <config>-calibrated-kh<n>.tbl carry the best values; evaluations.csv, result.json and best_<variable>.csv include them. rubem calibrate warns when dis.nstp is above 1 (see below).
  • Documentation: new doc/source/groundwater.rst (exchange equations, layer convention, every key with units, rules, outputs, run directory, root-depth coupling, limitations, references), the MODFLOW keys and template in the user guide, "MODFLOW parameters" in calibration.rst, changelog entry, README bullet. A test validates every JSON example of the groundwater page against the schema.

Resolution of the five review observations on the prototype (the extension numbers layers bottom-up, the observations were written top-down, hence the new convention):

  1. Heads: the schema binds each layer to its own initial_head; the exercise configuration puts head3.map on layer 1 (top) and head1.map on layer 3 (base) as asked, and the validation reports heads below their layer bottom.
  2. Bottoms: top_model > botton3 > botton2 > botton is enforced cell by cell on active columns; layer 1 bottom = botton3, layer 3 bottom = botton.
  3. RIV: nothing was hard-coded in the prototype code (the example JSON put the river on PCRaster layer 3, which is the top); the river entry now names layers: [1] and the diagnostic reports the 383 of 2277 Batalha river cells whose bed lies outside layer 1 (315 below the layer bottom, 284 of them in layer 2's interval and 31 in layer 3's, 68 above the model top), the "83 %" figure.
  4. GHB: one entry with layers: [1, 2, 3] and the same maps; the total boundary conductance per column is then three times the map value, documented, not split.
  5. WET: WETDRY is read only for LAYCON 1 or 3 layers (BCF), so wetting on the three layers needs laytype 3 on the lower ones; the configuration refuses a listed layer whose LAYCON is not 1 or 3.

Deviations from the approved plan, each recorded in the ledger with its reason:

  • dis.nstp defaults to 1 (the plan said 5, the scientist's value, still available explicitly). With several time steps per period a solver failure at an earlier time step makes the extension end the whole process inside run() ("Can not open head value result file"), before converged() can be asked; with one time step the failure raises the RuntimeError the calibrator records as a failed evaluation ranked last. rubem calibrate warns on nstp > 1 instead of refusing it.
  • An existing <output>/modflow directory is refused instead of reused (the run removes the directory at the end).
  • Enabled packages need at least one entry and enabled wetting needs map (the prototype's source_boundary_layer/multiplier alternative is gone); laytype is required; numeric properties are strict floats; the legacy to_dict() writes MODFLOW: null like TABLES.lai_max.
  • kh interval rows ([3,5]) are not calibratable (no name); the catalog names only the storage the layer's LAYCON reads.
  • .codespellrc ignores "botton" (the scientist's file names used in tests and docs), so a real misspelling of "bottom" in rubem/ or tests/ now passes the blocking spelling step.
  • Initial heads equal to a MODFLOW no-data marker (-888, -999, -999.9, -999.99, -9999) in an active cell are a blocking problem (added after the Batalha exercise showed the marker propagating into the head outputs).

Related Issue

Motivation and context

  • RUBEM's saturated zone is a lumped linear reservoir per cell; basins with a regional aquifer, river-aquifer exchange or lateral boundaries need a groundwater model. The prototype branches are 116 commits behind main, predate the Pydantic configuration layer, rubem.api and rubem calibrate, write MODFLOW files into the working directory, bypass the output formats and import the extension unconditionally.

How has this been tested

  • python -m pytest --ignore=tests/integration/doc -n 2 -q -p no:cacheprovider: 2238 passed, 2 skipped (the byte-exact golden test, CI-only, and the ipojuca dataset smoke) on this branch (Linux, Python 3.13, PCRaster 4.4.2, GDAL 3.11). New tests: test_modflow_settings.py (148), test_modflow_inputs.py (60), test_module_modflow.py (66, mocked extension), test_dynamic_model_modflow.py (22), test_modflow_coupling.py (44 with the real mf2005: the prototype's analytic RIV/GHB/DRN/RCH cases, a three-layer run asserting the BCF LAYCON line, full coupled runs, default-path identity, non-convergence in a child process), test_modflow_parameters.py (34), test_deps.py (+21), test_modflow_documentation.py (4), plus additions to the calibration and CLI tests (a real tiny search with modflow.layers.1.specific_yield searched and modflow.layers.1.kh.2 fixed).
  • uvx ruff@0.16.4 check ., uvx ruff@0.16.4 format --check ., codespell rubem tests: clean. Sphinx was not run locally (not installed); the CI docs job is the check.
  • Batalha basin exercise (LabSid dataset, three layers, 30 m geographic grid, 9 monthly steps 08/2023-04/2024, configuration written under the new schema following the scientist's instruction top = suffix 3, with the class maps and tables of calibracao/HKs): validation reports the riverbed diagnostic above and heads below the layer bottom (5136 cells of head3.map on layer 1, 5041 of them the -888 sentinel, 277 on layer 2, 18 on layer 3). With the sentinel rule added at the end of this branch the 5041 sentinels are a blocking problem, so the exercise runs with --allow-blocking-problems. The run as configured (wetting with wet.map = 1 everywhere on layer 1, nstp 5) does not converge in the first period (1334 iterations, 803609 dry and 797756 wet cell conversions, budget discrepancy 43 %). Sensitivity probes (two steps, nstp 1):
Probe (two steps, nstp 1, --allow-blocking-problems) Result
V0: plan configuration (top = suffix 3, kh from HKs, KX vertical, wetting on layer 1) did not converge in period 1 (13 min, budget discrepancy 67 %)
V1: V0 with KX as horizontal and KY as vertical (the scientist's base JSON) did not converge (18 min, discrepancy 1.7 %)
V2: heads and K with suffix 1 on top (head1, HK1, KX1 on layer 1) did not converge (12 min, discrepancy 0.28 %)
V3: V0 without wetting converged, both steps, 19 s; bfw up to 1228 mm (river cells), aquifer-to-river 1.19e4 m3/day, heads 448-686 m on layers 1-2
V4: V1 with GHB on the top layer only (the prototype's example) did not converge within 9 min (run killed)
V5: V2 with KX as horizontal and KY as vertical did not converge within 9 min (run killed)
V6: V2 without wetting (suffix 1 on top) converged, both steps, 21 s
V7: V1 without wetting (KX horizontal) converged, both steps, 20 s
V8: V0 with WETDRY = -1 on the active domain (the prototype's multiplier: -1.0, rewetting from below only) did not converge (7 min, discrepancy 69 %)

Every variant with the BCF wetting enabled fails in the first period and every variant without it converges in about 20 s, whatever the suffix or conductivity convention: the wetting configuration together with the 5041 top-layer cells that start dry is what stops the solver, and the layer conventions do not decide convergence. The exercise can proceed today with wetting off, as a modelling choice for the scientist to confirm; the other questions remain.

The calibration exercise and the dataset smoke test are therefore not in this PR; they follow once the scientist settles the questions below.

Left for the reviewer and for the scientist (LINAMARIAOSORIO):

  1. Which file suffix is the top layer for the heads and for the conductivities? The bottoms are numbered bottom-up (botton = base), but KY1..3 (KH 0.5/0.1) and the HK1..3 class maps put the Marília-rich map at suffix 1 and Adamantina only at suffix 3, and head1 > head2 > head3 on average, which reads top-down.
  2. KX<n>.map carry the VANIS values of variacoes_parametrosMODFLOW.xlsx (4.81, 5.19): anisotropy ratios (vertical K = KH / VANIS) or vertical conductivities in m/d?
  3. The -888 values inside the active domain (5041 cells of head3.map, 277 of head2.map, 18 of head1.map; the validation now blocks them as no-data markers): dry cells, absent layer or no-data?
  4. Which surface defines the depth to the water table for the root-depth coupling: the DEM (prototype) or top_model (they differ by -143 to +93 m)?
  5. Which observed series and station match the window, and their units?
  6. wet.map is 1 on every active cell (positive WETDRY: cells rewet from the side and from below with a 1 m threshold); the prototype's multiplier: -1.0 was never applied when a map was given. The probes above show the run converging without wetting.
  7. GHB on the three layers triples the boundary conductance; hclose 5.0 and rclose 3.0 are the prototype's values.

Screenshots

  • N/A

LINAMARIAOSORIO and others added 10 commits September 23, 2026 12:20
Port of rubem/hydrological_processes/_modflow.py, rubem/configuration/modflow_configuration.py and the user-guide text from the branches feat_modflow_module and feat_calibrator_modflow at commit da41faa (commits ed24a30, 0954a20, 079b833, 299a51e, 63ebddb, c70370f, da41faa). The modules are not wired into the model yet; the configuration schema, validation, dynamic-model coupling, calibrator support, tests and documentation follow in the next commits of #356.
ruff format only, no code change; refs #356.
Rework the prototype MODFLOW schema into ModflowSettings: layers are listed
from the top down and numbered like MODFLOW (layer 1 is the top), each with
its own bottom map under a model top; pcraster_layer() and user_layer()
convert to and from the bottom-up numbering of the PCRaster extension.
RIV, GHB and DRN entries and the BCF wetting attach to explicit layer lists.

The section is strict in both formats, so the prototype keys that no longer
exist (wells, recharge, dis units, solver type, fail_on_non_convergence,
baseflow_from_river_leakage, per-layer top, wetting multiplier and source
layer) are refused by name instead of silently disabling a package.
enabled is a boolean (0/1 still accepted). When the section is enabled,
every rule is checked and reported at once: model top and layers, unique
layer names, the river package (its leakage is the baseflow), layer numbers
in range and once per package, storage per LAYCON in transient runs, LAYCON
1 only on the top layer, wetting only on LAYCON 1 or 3 layers and with its
WETDRY map, a mask for a numeric river conductance, the water-table layer
and the minimum root depth table of the root-depth coupling.

The section is the MODFLOW key of the legacy file (null when absent) and
the modflow key of format 1.0 (left out when absent); both anchor its paths
on the configuration directory, the conversions carry it, and the migration
rebases its paths like the others. ModelConfiguration exposes it with
modflow_enabled and reports a present but disabled section as ignored.

Refs #356
An enabled MODFLOW section is now checked when the configuration loads,
so a coupled run or a calibration refuses bad inputs up front instead of
failing inside the PCRaster MODFLOW extension, which aborts the process
on some of them.

check_modflow_inputs (rubem/validation/modflow_inputs.py) always reports,
as blocking, a missing or empty file the run reads (files of disabled
packages, wetting and root-depth coupling are not read) and a missing
MODFLOW runtime. With validate_input it reads every raster on the clone
and reports: rasters that cannot be read or do not share the clone
geometry (PCRaster reads a map of another size silently), boundary values
other than -1/0/1, a top or bottom missing or not strictly decreasing on
columns active in a layer, heads, conductivities and storage missing on
active cells, heads below the layer bottom (blocking in every cell, a
warning in some), conductivity classes not covered by a positive table
value, an enabled package layer without cells, package heads missing on
package cells, riverbeds outside the elevation interval of their layer
(non-blocking count) and a Dpz_min table that does not cover the soil
classes or leaves (0, Zr].

rubem/_deps.py gains the groundwater tier: missing_groundwater_deps,
groundwater_deps_message, resolve_mf2005 (PATH, then the interpreter
prefix where conda installs mf2005) and ensure_mf2005_on_path.

A configuration without the section runs no check and reports the same
problems as before.

Refs #356
Rework the ported groundwater module around the validated ModflowSettings.
ModflowGroundwater(settings, cell_area_m2, run_directory, logger) reads every
input once in initialize() with read_field, converts the top-down layer
numbers of the configuration with pcraster_layer(), and gives the extension
its packages in the order DIS, BAS, BCF, wetting, solver, then RIV, GHB and
DRN. The stress packages are set once: the extension keeps them for every
later run, so no map is read again per step.

Conductance is zeroed outside the mask and the layer boundary, and the
active cells are counted per package layer: a layer without cells is never
set nor read, because the extension ends the process on a getter for a
package layer without cells. Columns inactive in every layer keep the
prototype's synthetic 1 m elevations. The kh lookup still reads a fresh copy
of its table (PCRaster caches tables by name), now inside the run directory.

run_step() updates PERLEN when transient, sets RCH (mm/step to m/day, highest
active cell), runs mf2005 in the run directory so no pcrmf.* file lands in
the working directory, raises RuntimeError on non-convergence (the next run
would end the process), and returns the river exchange, the baseflow in mm
and the heads, storage, drain flow and water-table head keyed by the
configured layer numbers. The water table reads the cached boundaries and
surfaces instead of re-reading maps each step.

pcraster.initialise is imported only when a model starts, after
ensure_mf2005_on_path(), and a missing extension or executable raises
RuntimeError with the installation guidance. The prototype's own validation
pass is gone (rubem.validation.modflow_inputs owns the rules); the module
only refuses missing values on the cells it uses and layers that are not
stacked, which the extension cannot take whatever validate_input says.

Tests: a mocked extension for the call order, the numbering conversion,
set-once stress data, skipped empty layers, unit conversions and the water
table methods; the prototype's analytic 3x3 RIV, GHB, DRN and RCH balances
against the real mf2005 (skipped when resolve_mf2005() finds none).

Refs #356
When the MODFLOW section is enabled, initial() creates
<output directory>/modflow, starts ModflowGroundwater there with the days
of the first month and, with the root-depth coupling, keeps the DEM as the
terrain surface and reads Dpz_min per soil class. The module is imported
inside initial(), so a run without the section never loads it.

Each step pushes the RUBEM recharge to MODFLOW for the days of the month
and takes the aquifer-to-river RIV leakage of the result as bfw; the
saturated-zone reservoir is then not updated. The root-depth coupling uses
the water table of the previous step (one-step lag): the effective root
depth is the water-table depth below the terrain bounded by Dpz_min and
Zr, and only the vegetated-area water stress coefficient sees the storages
of that root fraction; the bare soil keeps the whole-rootzone coefficient
and the soil balance is unchanged.

The enabled diagnostics go through the same two format branches as the
output variables (mfh<n>, mfaq2rv, mfrv2aq, mfrvnet, mfst<n>, mfdrn<n>,
and mfwt, mfgwd, mfzr, mfzfrac with output.root_depth), so an empty format
set writes nothing. The run directory is removed after the last step's
report and kept when a step raised, with pcrmf.lst for inspection.

Without the section, or with it disabled, every output is identical to
the run before this change.

Refs #356
A configuration that enables MODFLOW is calibrated with the coupled model
in every evaluation. Its MODFLOW parameters join the search only when the
caller names them, so a calibration without MODFLOW names searches and
reports exactly what it did before.

rubem.calibration.modflow_parameters builds the catalog of a section:
modflow.layers.<n>.specific_yield and .specific_storage (numbers the
transient run reads for the layer's LAYCON), modflow.layers.<n>.kh.<class>
(numeric rows of the layer's conductivity lookup table) and
modflow.river.<i>.conductance (numeric entries). ModflowCatalog.apply
writes a candidate into a format 1.0 document: numbers in place, named
classes into a rewritten copy of the table whose path replaces the
configured one.

decision_space() takes the catalog. A MODFLOW name is searched only when
bounded (finite, minimum below maximum, strictly positive, at most 1 for
a specific yield), is appended after the eight hydrological parameters,
and a fixed one keeps its value in every candidate; an unknown name lists
the known ones, and a MODFLOW name without MODFLOW enabled is refused.
The positions of w_1 and w_2, hence the weights constraint, are unchanged.

The runner builds the catalog after the validated load, resolves the
decision space there, starts from the configured MODFLOW values, puts
mf2005 on the PATH the workers inherit, adds the MODFLOW names to
evaluations.csv after x, reruns the best candidate with its MODFLOW
values, and writes them into the calibrated configuration with its
<name>-kh<n>.tbl tables. A dead worker of a coupled calibration also
names non-convergence before the last time step of a period (dis.nstp >
1), which ends the worker process instead of failing the evaluation.

The --bound and --fix help texts name the MODFLOW parameters.

Refs #356
Add the Groundwater Coupling page (doc/source/groundwater.rst, in the
toctree after the calibration page): what the coupling exchanges (recharge
to RCH in m/day, the aquifer-to-river RIV leakage as bfw in mm, the
saturated-zone reservoir no longer updated, GHB/DRN/wetting shaping the
heads only), the runtime it needs, the top-down layer numbering, every key
of the section with its type, default and unit, the rules the configuration
and the validation enforce, the raster diagnostics, the run directory, the
experimental root-depth coupling and the limitations, among them the solver
failure before the last time step of a period with dis.nstp above 1, which
ends the process instead of raising.

Replace the prototype text appended to the user guide, which numbered the
layers from the bottom up and described keys that no longer exist, with a
MODFLOW section listing the keys in the style of the other entries, add the
section (disabled) to the configuration file template, and state what the
coupling changes for bfw, for -s and in the calibrate help.

Add a MODFLOW parameters section to the calibration page (names, the values
they may take, when a name exists, the mandatory bound, an example) and
update the sentences that the MODFLOW names make incomplete: what a
calibration changes, the budget, the columns of evaluations.csv,
result.json, the calibrated configuration and its kh tables, the process
model and the dead-worker message.

Add the changelog entry and a README feature bullet, and a unit test that
parses every JSON example of the new page and validates its MODFLOW
section, so the page cannot drift from the schema.

Refs #356
Default dis.nstp to 1, one MODFLOW time step per stress period. The
PCRaster MODFLOW extension reads the heads of a period only when its last
time step converged: with several time steps a solver failure at an earlier
one leaves no head file, and the extension ends the whole process inside
run() ("Can not open head value result file") before converged() can be
asked. With one time step a failure is always at the last one, and the run
raises the RuntimeError that names the period and the listing, which the
calibrator records as a failed evaluation ranked last, as decided. The
scientist's value, 5, stays available as an explicit choice with the
documented limitation.

Warn in rubem calibrate when the configuration enables MODFLOW with
dis.nstp above 1: a candidate that fails before the last time step then
ends its worker process, and the pool cannot replace it, so the whole
search ends on one bad candidate. The value is not refused because the
discretization of a calibration should be the one of the production run.

Tests: the child-process non-convergence test now runs the default section
and asserts the raised error, and the former characterization test pins the
opt-in nstp 5 case; the calibrator warning has a positive and a complement
case; the defaults and the mocked DIS calls read 1. Documentation: the dis
table, the Limitations entry, the user guide entry, the calibration
warning and the changelog state the default and the warning.

Refs #356
An initial head equal to -888, -999, -999.9, -999.99 or -9999 in an active cell is a blocking problem that names the layer, the count and the first cell. The extension reads such a marker as a head: the cell starts dry and the marker reaches the head outputs, which the Batalha exercise showed (5041 cells of one map at -888 came out as heads of -1393 m). The below-bottom warning no longer counts those cells; a marker on an inactive cell stays accepted. Documented in the blocking rules of the groundwater page and in the changelog.

Refs #356
@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.03754% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.19%. Comparing base (87818d9) to head (1da031a).

Files with missing lines Patch % Lines
rubem/validation/modflow_inputs.py 92.92% 11 Missing and 11 partials ⚠️
rubem/calibration/parameters.py 97.67% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #357      +/-   ##
==========================================
+ Coverage   93.36%   94.19%   +0.83%     
==========================================
  Files          68       72       +4     
  Lines        5260     6394    +1134     
  Branches      675      888     +213     
==========================================
+ Hits         4911     6023    +1112     
- Misses        274      285      +11     
- Partials       75       86      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optional MODFLOW groundwater coupling through the PCRaster MODFLOW extension

2 participants