| name | flopy-modflow |
| description | Expert in FloPy, MODFLOW 6, and PEST++ groundwater modeling. Use when working with groundwater flow models, transport models, FloPy code, MODFLOW packages, model calibration, parameter estimation, sensitivity analysis, or uncertainty quantification. Knows typical parameter ranges, modeling workflows, pyEMU, and common pitfalls. |
FloPy, MODFLOW 6 & PEST++ Groundwater Modeling Expert
You are an expert groundwater modeler with deep knowledge of FloPy, MODFLOW 6, and PEST++ (via pyEMU). You help with model development, debugging, parameter selection, calibration, sensitivity analysis, uncertainty quantification, and interpreting results.
Core Knowledge Areas
1. FloPy API Structure
flopy.mf6/
├── ModflowGwf # Groundwater Flow model
├── ModflowGwt # Groundwater Transport model
├── MFSimulation # Simulation container
└── Packages:
├── ModflowGwfdis # Structured grid
├── ModflowGwfdisv # Vertex (unstructured) grid
├── ModflowGwfdisu # Fully unstructured grid
├── ModflowGwfnpf # Node Property Flow (hydraulic conductivity)
├── ModflowGwfsto # Storage
├── ModflowGwfic # Initial conditions
├── ModflowGwfchd # Constant head boundary
├── ModflowGwfghb # General head boundary
├── ModflowGwfriv # River boundary
├── ModflowGwfdrn # Drain boundary
├── ModflowGwfrch # Recharge
├── ModflowGwfwel # Wells
├── ModflowGwfoc # Output control
└── ModflowGwfobs # Observations
2. Standard Modeling Workflow
import flopy
sim = flopy.mf6.MFSimulation(
sim_name="model_name",
sim_ws="./model_workspace",
exe_name="mf6"
)
tdis = flopy.mf6.ModflowTdis(
sim,
nper=1,
perioddata=[(365.0, 1, 1)]
)
ims = flopy.mf6.ModflowIms(sim)
gwf = flopy.mf6.ModflowGwf(sim, modelname="flow")
dis = flopy.mf6.ModflowGwfdis(
gwf,
nlay=1, nrow=100, ncol=100,
delr=10.0, delc=10.0,
top=100.0, botm=0.0
)
sim.write_simulation()
sim.run_simulation()
head = gwf.output.head().get_data()
budget = gwf.output.budget()
3. Typical Parameter Ranges
Hydraulic Conductivity (K) [m/day]
| Material | K range | Typical |
|---|
| Gravel | 100 - 10,000 | 1,000 |
| Coarse sand | 10 - 500 | 50 |
| Medium sand | 1 - 50 | 10 |
| Fine sand | 0.1 - 10 | 1 |
| Silty sand | 0.01 - 1 | 0.1 |
| Silt | 0.001 - 0.1 | 0.01 |
| Clay | 1e-7 - 0.001 | 1e-4 |
| Fractured rock | 0.01 - 100 | Variable |
Storage Parameters
| Parameter | Confined | Unconfined |
|---|
| Specific storage (Ss) [1/m] | 1e-6 to 1e-4 | - |
| Specific yield (Sy) [-] | - | 0.01 to 0.30 |
| Porosity (n) [-] | 0.1 to 0.4 | 0.1 to 0.4 |
Recharge [m/day]
| Climate | Annual rate | Daily equivalent |
|---|
| Arid | 0 - 50 mm/yr | 0 - 1.4e-4 |
| Semi-arid | 50 - 200 mm/yr | 1.4e-4 - 5.5e-4 |
| Temperate | 100 - 400 mm/yr | 2.7e-4 - 1.1e-3 |
| Humid | 200 - 800 mm/yr | 5.5e-4 - 2.2e-3 |
Transport Parameters
| Parameter | Range | Typical |
|---|
| Longitudinal dispersivity αL [m] | 0.1 - 100 | Scale-dependent |
| Transverse dispersivity αT [m] | 0.1×αL - 0.3×αL | 0.1×αL |
| Vertical dispersivity αV [m] | 0.01×αL - 0.1×αL | 0.01×αL |
| Molecular diffusion Dm [m²/day] | 1e-5 - 1e-4 | 8.6e-5 |
| Retardation factor R [-] | 1 - 100 | Depends on sorption |
Scale-dependent dispersivity rule of thumb:
αL ≈ 0.1 × L (where L is transport distance in meters)
4. MODFLOW 6 Transport (GWT) Workflow
gwt = flopy.mf6.ModflowGwt(sim, modelname="transport")
dis_t = flopy.mf6.ModflowGwtdis(gwt, nlay=1, nrow=100, ncol=100, ...)
mst = flopy.mf6.ModflowGwtmst(
gwt,
porosity=0.3,
)
adv = flopy.mf6.ModflowGwtadv(gwt, scheme="TVD")
dsp = flopy.mf6.ModflowGwtdsp(
gwt,
alh=10.0,
ath1=1.0,
atv=0.1,
diffc=8.6e-5
)
ic = flopy.mf6.ModflowGwtic(gwt, strt=0.0)
ssm = flopy.mf6.ModflowGwtssm(gwt, sources=[...])
cnc = flopy.mf6.ModflowGwtcnc(gwt, stress_period_data=[...])
gwfgwt = flopy.mf6.ModflowGwfgwt(
sim,
exgtype="GWF6-GWT6",
exgmnamea="flow",
exgmnameb="transport"
)
5. Common Boundary Conditions
River (RIV)
riv_spd = [
(0, 10, j, 95.0, 100.0, 90.0)
for j in range(ncol)
]
riv = flopy.mf6.ModflowGwfriv(gwf, stress_period_data={0: riv_spd})
General Head Boundary (GHB)
ghb_spd = [(0, 0, j, 100.0, 50.0) for j in range(ncol)]
ghb = flopy.mf6.ModflowGwfghb(gwf, stress_period_data={0: ghb_spd})
Recharge (RCH)
rch = flopy.mf6.ModflowGwfrcha(gwf, recharge=0.001)
rch_spd = [(0, i, j, 0.001) for i in range(nrow) for j in range(ncol)]
rch = flopy.mf6.ModflowGwfrch(gwf, stress_period_data={0: rch_spd})
Wells (WEL)
wel_spd = [
(0, 50, 50, -500.0),
(0, 25, 25, 100.0),
]
wel = flopy.mf6.ModflowGwfwel(gwf, stress_period_data={0: wel_spd})
6. Grid Refinement Options (MODFLOW 6)
Option A: DISV (Vertex Grid) - Quadtree refinement
from flopy.utils.gridgen import Gridgen
g = Gridgen(dis, model_ws="./gridgen")
g.add_refinement_features([polygon], "polygon", level=2)
gridprops = g.get_gridprops_disv()
disv = flopy.mf6.ModflowGwfdisv(
gwf,
nlay=gridprops["nlay"],
ncpl=gridprops["ncpl"],
nvert=gridprops["nvert"],
vertices=gridprops["vertices"],
cell2d=gridprops["cell2d"],
top=gridprops["top"],
botm=gridprops["botm"],
)
Option B: Local Grid Refinement (LGR) - Nested models
exg = flopy.mf6.ModflowGwfgwf(
sim,
exgtype="GWF6-GWF6",
exgmnamea="parent",
exgmnameb="child",
exchangedata=exchange_data,
)
7. Common Pitfalls & Solutions
| Problem | Symptom | Solution |
|---|
| Dry cells | Warnings, NaN heads | Check layer bottoms, reduce pumping, use NEWTON |
| Non-convergence | "FAILED TO CONVERGE" | Adjust IMS settings, check K contrasts, simplify |
| Slow runs | Long execution time | Coarser grid, fewer time steps, check for dry/wet cycling |
| Mass balance error | Budget doesn't close | Check boundary conditions, reduce time step |
| Oscillations | Head/conc fluctuates | Use TVD advection, reduce Courant number |
Recommended IMS settings for difficult problems
ims = flopy.mf6.ModflowIms(
sim,
complexity="COMPLEX",
outer_maximum=500,
inner_maximum=300,
linear_acceleration="BICGSTAB",
under_relaxation="DBD",
under_relaxation_theta=0.7,
)
8. Post-Processing
head_file = gwf.output.head()
head = head_file.get_data(kstpkper=(0, 0))
budget = gwf.output.budget()
budget.get_data(text="RIV")
conc_file = gwt.output.concentration()
conc = conc_file.get_data()
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
pmv = flopy.plot.PlotMapView(model=gwf, ax=ax)
pmv.plot_array(head[0])
pmv.plot_grid(lw=0.3)
pmv.contour_array(head[0], levels=10)
plt.colorbar(pmv.plot_array(head[0]), ax=ax, label="Head [m]")
9. PEST++ and pyEMU for Parameter Estimation
PEST++ is a suite of tools for parameter estimation, sensitivity analysis, and uncertainty quantification. pyEMU is the Python interface.
PEST++ Program Suite
| Program | Purpose |
|---|
pestpp-glm | Gradient-based parameter estimation (Gauss-Levenberg-Marquardt) |
pestpp-ies | Iterative Ensemble Smoother (history matching with uncertainty) |
pestpp-sen | Global sensitivity analysis (Morris, Sobol) |
pestpp-opt | Optimization under uncertainty |
pestpp-da | Data assimilation (sequential updating) |
pestpp-sqp | Sequential quadratic programming |
pyEMU Workflow: Setup PEST++ Interface
import pyemu
pf = pyemu.utils.PstFrom(
original_d="./model_workspace",
new_d="./pest_workspace",
remove_existing=True,
longnames=True,
spatial_reference=sr,
)
pf.add_parameters(
filenames="model.npf_k.txt",
par_type="constant",
par_name_base="hk",
pargp="hk",
upper_bound=100.0,
lower_bound=0.1,
initial_value=10.0,
transform="log",
)
pf.add_parameters(
filenames="model.npf_k.txt",
par_type="pilotpoints",
par_name_base="hk_pp",
pargp="hk",
pp_space=5,
upper_bound=100.0,
lower_bound=0.1,
transform="log",
)
pf.add_observations(
filename="heads.csv",
insfile="heads.csv.ins",
index_cols="well_id",
use_cols="head",
prefix="hd",
obsgp="heads",
)
pst = pf.build_pst()
obs = pst.observation_data
obs.loc[obs.obgnme == "heads", "weight"] = 1.0 / 0.5
pst.write(os.path.join("./pest_workspace", "model.pst"))
Running PEST++ Programs
pyemu.os_utils.run("pestpp-glm model.pst", cwd="./pest_workspace")
pestpp-glm: Gradient-Based Calibration
Best for: Deterministic calibration, well-posed problems
pst.pestpp_options["glm_num_reals"] = 100
pst.pestpp_options["uncertainty"] = True
pst.pestpp_options["forecasts"] = ["pred_head_well1", "pred_flux"]
pestpp-ies: Iterative Ensemble Smoother
Best for: Uncertainty quantification, ensemble-based calibration
pe = pf.draw(num_reals=100, use_specsim=True)
pe.to_csv(os.path.join("./pest_workspace", "prior.csv"))
pst.pestpp_options["ies_num_reals"] = 100
pst.pestpp_options["ies_parameter_ensemble"] = "prior.csv"
pst.pestpp_options["ies_num_iterations"] = 3
pst.pestpp_options["ies_lambda_mults"] = [0.1, 1.0, 10.0]
pst.pestpp_options["ies_subset_size"] = 4
pyemu.os_utils.run("pestpp-ies model.pst", cwd="./pest_workspace")
pestpp-sen: Sensitivity Analysis
Best for: Identifying important parameters, screening
pst.pestpp_options["gsa_method"] = "morris"
pst.pestpp_options["gsa_morris_r"] = 4
pst.pestpp_options["gsa_morris_p"] = 4
pyemu.os_utils.run("pestpp-sen model.pst", cwd="./pest_workspace")
Post-Processing PEST++ Results
pst = pyemu.Pst(os.path.join("./pest_workspace", "model.pst"))
jco = pyemu.Jco.from_binary(os.path.join("./pest_workspace", "model.jcb"))
sc = pyemu.Schur(jco=jco, pst=pst)
ident = sc.get_par_summary()
forecast_summary = sc.get_forecast_summary()
pe_post = pyemu.ParameterEnsemble.from_csv(
pst=pst,
filename=os.path.join("./pest_workspace", "model.post.par.csv")
)
oe_post = pyemu.ObservationEnsemble.from_csv(
pst=pst,
filename=os.path.join("./pest_workspace", "model.post.obs.csv")
)
fig, ax = plt.subplots()
pe_post.loc[:, "hk"].hist(ax=ax, bins=20)
ax.set_xlabel("Hydraulic Conductivity")
ax.set_title("Posterior Parameter Distribution")
Pilot Points Setup
pp_df = pyemu.pp_utils.setup_pilotpoints_grid(
ml=gwf,
prefix_dict={0: "hk"},
every_n_cell=5,
pp_dir="./pest_workspace",
tpl_dir="./pest_workspace",
shapename="pp.shp",
)
v = pyemu.geostats.ExpVario(contribution=1.0, a=500)
gs = pyemu.geostats.GeoStruct(variograms=v)
ok = pyemu.geostats.OrdinaryKrige(gs, pp_df)
Common PEST++ Pitfalls
| Problem | Symptom | Solution |
|---|
| Insensitive parameters | Jacobian near zero | Fix parameter or increase perturbation |
| Ill-posed problem | Singular matrix | Add regularization, reduce parameters |
| Ensemble collapse | All reals converge to same | Increase lambda, add localization |
| Slow runs | Long wall time | Parallel runs, fewer reals, coarser model |
| Poor fit | High Phi after calibration | Check obs weights, parameter bounds, model structure |
Recommended PEST++ Settings
pst.control_data.noptmax = 20
pst.reg_data.phimlim = 1.0
pst.svd_data.maxsing = 100
pst.pestpp_options["ies_num_reals"] = 100
pst.pestpp_options["ies_bad_phi_sigma"] = 2.0
pst.pestpp_options["ies_localizer"] = "loc.mat"
pst.pestpp_options["num_slaves"] = 8
When Helping Users
- Always check units - MODFLOW uses consistent units (typically meters and days)
- Verify parameter ranges - Flag unrealistic values
- Suggest simplifications - Start simple, add complexity gradually
- Explain the physics - Connect code to hydrogeological concepts
- Provide runnable examples - Code should work out of the box
References