Skip to content

Commit 0980606

Browse files
authored
feat(cellbudgetfile): support aux vars with full3D in get_data() (#2776)
Add a 'variable' parameter (default "q") to get_data() and get_record() to allow specifying the field to extract to a 3D grid-shaped array. Previously "q" was returned unconditionally. 'variable' is only supported when full3D=True and the method's default behavior is unchanged. sat = cbc.get_data(text="DATA-SAT", full3D=True, variable="sat")[0] This is consistent with the existing `variable` parameter in get_ts(), added in #2568 Addresses #2774.
1 parent 018bd9c commit 0980606

2 files changed

Lines changed: 126 additions & 12 deletions

File tree

autotest/test_cellbudgetfile.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,94 @@ def test_cellbudgetfile_get_ts_backwards_compatible_idx_format(
11581158
)
11591159

11601160

1161+
@pytest.mark.requires_exe("mf6")
1162+
def test_cellbudgetfile_full3D_aux_var(function_tmpdir):
1163+
"""
1164+
Reproduce GitHub issue #2774: get_data(full3D=True) should return
1165+
correct values for auxiliary variables (e.g. sat), not just for q.
1166+
"""
1167+
from flopy.mf6 import (
1168+
MFSimulation,
1169+
ModflowGwf,
1170+
ModflowGwfchd,
1171+
ModflowGwfdis,
1172+
ModflowGwfic,
1173+
ModflowGwfnpf,
1174+
ModflowGwfoc,
1175+
ModflowIms,
1176+
ModflowTdis,
1177+
)
1178+
1179+
sim_name = "test_full3d_sat"
1180+
sim = MFSimulation(sim_name=sim_name, sim_ws=function_tmpdir, exe_name="mf6")
1181+
ModflowTdis(sim, nper=2, perioddata=[(1.0, 1, 1.0), (1.0, 1, 1.0)])
1182+
ModflowIms(sim)
1183+
gwf = ModflowGwf(sim, modelname=sim_name, save_flows=True)
1184+
nlay, nrow, ncol = 1, 5, 5
1185+
ModflowGwfdis(
1186+
gwf, nlay=nlay, nrow=nrow, ncol=ncol, delr=10.0, delc=10.0, top=10.0, botm=[0.0]
1187+
)
1188+
ModflowGwfic(gwf, strt=5.0)
1189+
ModflowGwfnpf(gwf, k=1.0, icelltype=1, save_saturation=True)
1190+
ModflowGwfchd(gwf, stress_period_data=[[(0, 0, 0), 9.0], [(0, 4, 4), 1.0]])
1191+
ModflowGwfoc(
1192+
gwf,
1193+
budget_filerecord=f"{sim_name}.cbc",
1194+
head_filerecord=f"{sim_name}.hds",
1195+
saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")],
1196+
)
1197+
1198+
sim.write_simulation()
1199+
success, _ = sim.run_simulation(silent=True)
1200+
assert success
1201+
1202+
cbc = gwf.output.budget()
1203+
1204+
# non-full3D
1205+
sat_rec = cbc.get_data(text="DATA-SAT")
1206+
assert len(sat_rec) > 0
1207+
assert "sat" in sat_rec[0].dtype.names
1208+
assert "q" in sat_rec[0].dtype.names
1209+
assert np.allclose(sat_rec[0]["q"], 0.0), "q should be zero in DATA-SAT records"
1210+
assert not np.allclose(sat_rec[0]["sat"], 0.0), "sat values should be non-zero"
1211+
assert not np.allclose(sat_rec[0]["sat"], sat_rec[0]["sat"][0]), (
1212+
"sat values should vary across cells"
1213+
)
1214+
1215+
# full3D with default variable="q"
1216+
q_3d = cbc.get_data(text="DATA-SAT", full3D=True)
1217+
assert q_3d[0].shape == (nlay, nrow, ncol)
1218+
assert np.allclose(np.ma.filled(q_3d[0], 0.0), 0.0)
1219+
1220+
# full3D with variable="sat"
1221+
sat_3d = cbc.get_data(text="DATA-SAT", full3D=True, variable="sat")
1222+
assert sat_3d[0].shape == (nlay, nrow, ncol)
1223+
assert not np.allclose(np.ma.filled(sat_3d[0], 0.0), 0.0)
1224+
1225+
# check recarray and full 3D array match
1226+
rec = sat_rec[0]
1227+
arr = sat_3d[0]
1228+
for node, sat_val in zip(rec["node"], rec["sat"]):
1229+
k = (node - 1) // (nrow * ncol)
1230+
rc = (node - 1) % (nrow * ncol)
1231+
r = rc // ncol
1232+
c = rc % ncol
1233+
np.testing.assert_allclose(
1234+
arr[k, r, c],
1235+
sat_val,
1236+
err_msg=f"3D SAT value mismatch at node {node}",
1237+
)
1238+
1239+
1240+
def test_cellbudgetfile_get_data_variable_without_full3D(example_data_path):
1241+
"""variable= is only meaningful with full3D=True; without it, raise ValueError."""
1242+
mf2005_model_path = example_data_path / "mf2005_test"
1243+
cbc = CellBudgetFile(mf2005_model_path / "test1tr.gitcbc")
1244+
with pytest.raises(ValueError, match="only used when full3D=True"):
1245+
cbc.get_data(text="WELLS", variable="IFACE")
1246+
cbc.close()
1247+
1248+
11611249
@pytest.mark.requires_exe("mf6")
11621250
def test_cellbudgetfile_write_preserves_aux_vars(dis_sim, function_tmpdir):
11631251
"""Test that write() method preserves auxiliary variables in imeth=6 records."""

flopy/utils/binaryfile/__init__.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,6 +2894,7 @@ def get_data(
28942894
paknam=None,
28952895
paknam2=None,
28962896
full3D=False,
2897+
variable="q",
28972898
) -> Union[list, np.ndarray]:
28982899
"""
28992900
Get data from the binary budget file.
@@ -2922,6 +2923,10 @@ def get_data(
29222923
If true, then return the record as a three dimensional numpy
29232924
array, even for those list-style records written as part of a
29242925
'COMPACT BUDGET' MODFLOW budget file. (Default is False.)
2926+
variable : str
2927+
The variable name to extract when full3D is True and the record
2928+
contains auxiliary variables (e.g. 'sat' for 'DATA-SAT' records,
2929+
or 'qx'/'qy'/'qz' for 'DATA-SPDIS' records). Default is 'q'.
29252930
29262931
Returns
29272932
-------
@@ -2999,8 +3004,14 @@ def get_data(
29993004
"get_data() missing 1 required argument: 'kstpkper', 'totim', "
30003005
"'idx', or 'text'"
30013006
)
3007+
if variable != "q" and not full3D:
3008+
raise ValueError(
3009+
"'variable' is only used when full3D=True. "
3010+
"To access a specific field without full3D, index the "
3011+
"returned recarray directly, e.g. get_data(...)[0]['sat']."
3012+
)
30023013
return [
3003-
self.get_record(idx, full3D=full3D)
3014+
self.get_record(idx, full3D=full3D, variable=variable)
30043015
for idx, t in enumerate(select_indices)
30053016
if t
30063017
]
@@ -3298,7 +3309,7 @@ def _cellid_to_node(self, cellids) -> list[int]:
32983309
nodes_0based = self.modelgrid.get_node(cellids)
32993310
return (np.array(nodes_0based) + 1).tolist()
33003311

3301-
def get_record(self, idx, full3D=False):
3312+
def get_record(self, idx, full3D=False, variable="q"):
33023313
"""
33033314
Get a single data record from the budget file.
33043315
@@ -3310,6 +3321,10 @@ def get_record(self, idx, full3D=False):
33103321
If true, then return the record as a three dimensional numpy
33113322
array, even for those list-style records written as part of a
33123323
'COMPACT BUDGET' MODFLOW budget file. (Default is False.)
3324+
variable : str
3325+
The variable name to extract when full3D is True and the record
3326+
contains auxiliary variables (e.g. 'sat' for 'DATA-SAT' records,
3327+
or 'qx'/'qy'/'qz' for 'DATA-SPDIS' records). Default is 'q'.
33133328
33143329
Returns
33153330
-------
@@ -3425,7 +3440,7 @@ def get_record(self, idx, full3D=False):
34253440
if self.verbose:
34263441
s += f"a list array of shape ({nlay}, {nrow}, {ncol})"
34273442
print(s)
3428-
return self.__create3D(data)
3443+
return self.__create3D(data, variable=variable)
34293444
else:
34303445
if self.verbose:
34313446
s += f"a numpy recarray of size ({nlist}, {2 + naux})"
@@ -3451,7 +3466,7 @@ def get_record(self, idx, full3D=False):
34513466
s += f"a numpy recarray of size ({nlist}, 2)"
34523467
print(s)
34533468
if full3D:
3454-
data = self.__create3D(data)
3469+
data = self.__create3D(data, variable=variable)
34553470
if self.modelgrid is not None:
34563471
return np.reshape(data, self.shape)
34573472
else:
@@ -3464,28 +3479,39 @@ def get_record(self, idx, full3D=False):
34643479
# should not reach this point
34653480
return
34663481

3467-
def __create3D(self, data):
3482+
def __create3D(self, data, variable="q"):
34683483
"""
3469-
Convert a dictionary of {node: q, ...} into a numpy masked array.
3484+
Convert list budget data into a numpy masked array.
34703485
Used to create full grid arrays when the full3D keyword is set
34713486
to True in get_data.
34723487
34733488
Parameters
34743489
----------
3475-
data : dictionary
3476-
Dictionary with node keywords and flows (q) items.
3490+
data : numpy recarray
3491+
Record array with at least 'node' and the specified variable field.
3492+
variable : str
3493+
The field name to map into the 3D array. Default is 'q'.
34773494
34783495
Returns
34793496
-------
34803497
out : numpy masked array
3481-
List contains unique simulation times (totim) in binary file.
3498+
Masked array of shape self.shape with values from the specified
3499+
variable mapped to their grid positions.
34823500
34833501
"""
3484-
out = np.ma.zeros(self.nnodes, dtype=data["q"].dtype)
3502+
if variable not in data.dtype.names:
3503+
raise ValueError(
3504+
f"variable '{variable}' not found in record. "
3505+
f"Available variables: {list(data.dtype.names)}"
3506+
)
3507+
out = np.ma.zeros(self.nnodes, dtype=data[variable].dtype)
34853508
out.mask = True
3486-
for [node, q] in zip(data["node"], data["q"]):
3509+
for node, val in zip(data["node"], data[variable]):
34873510
idx = node - 1
3488-
out.data[idx] += q
3511+
if variable == "q":
3512+
out.data[idx] += val
3513+
else:
3514+
out.data[idx] = val
34893515
out.mask[idx] = False
34903516
return np.ma.reshape(out, self.shape)
34913517

0 commit comments

Comments
 (0)