Skip to content

Commit 2092f70

Browse files
authored
fix(mfdataarray): support time array series (#2703)
Handle time-indexed and period-indexed transient array data independently. TAS data uses float keys (0.0, 6.0, 12.0) representing actual simulation times. Standard transient data uses integer keys (0, 1, 2) representing stress period indices. Only the latter were supported before. Also allow setting array data of unknown shape (like TAS) external, this was not supported before. Fix #2702.
1 parent 6ea8b8f commit 2092f70

2 files changed

Lines changed: 197 additions & 27 deletions

File tree

autotest/test_mf6_tas.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
Test TIMEARRAYSERIES (TAS) package functionality.
3+
4+
Tests for GitHub issue #2702:
5+
- Problem 1: Array entries repeated when accessing TAS data via .array property
6+
- Problem 2: set_all_data_external() not externalizing TAS arrays
7+
"""
8+
9+
from pathlib import Path
10+
11+
import numpy as np
12+
import pytest
13+
14+
import flopy
15+
16+
pytestmark = pytest.mark.mf6
17+
18+
19+
def test_tas_array_property(function_tmpdir):
20+
sim_ws = Path(function_tmpdir) / "tas_array_test"
21+
sim_ws.mkdir(exist_ok=True)
22+
23+
sim = flopy.mf6.MFSimulation(sim_name="tas_test", version="mf6", sim_ws=str(sim_ws))
24+
tdis = flopy.mf6.ModflowTdis(
25+
sim,
26+
time_units="DAYS",
27+
nper=3,
28+
perioddata=[(1.0, 1, 1.0), (10.0, 10, 1.0), (10.0, 10, 1.0)],
29+
)
30+
model = flopy.mf6.ModflowGwf(sim, modelname="model")
31+
ims = flopy.mf6.ModflowIms(sim, print_option="SUMMARY")
32+
sim.register_ims_package(ims, [model.name])
33+
dis = flopy.mf6.ModflowGwfdis(
34+
model,
35+
nlay=1,
36+
nrow=10,
37+
ncol=10,
38+
delr=100.0,
39+
delc=100.0,
40+
top=0.0,
41+
botm=-10.0,
42+
)
43+
ic = flopy.mf6.ModflowGwfic(model, strt=0.0)
44+
npf = flopy.mf6.ModflowGwfnpf(model, k=10.0)
45+
oc = flopy.mf6.ModflowGwfoc(model)
46+
rch = flopy.mf6.ModflowGwfrcha(model, recharge="TIMEARRAYSERIES rcharray")
47+
48+
rch_array_0 = 0.001 * np.ones((10, 10))
49+
rch_array_0[0, 0] = 0.01
50+
51+
rch_array_6 = 0.002 * np.ones((10, 10))
52+
rch_array_6[5, 5] = 0.02
53+
54+
rch_array_12 = 0.003 * np.ones((10, 10))
55+
rch_array_12[9, 9] = 0.03
56+
57+
tas = flopy.mf6.ModflowUtltas(
58+
rch,
59+
filename="model.rch.tas",
60+
tas_array={0.0: rch_array_0, 6.0: rch_array_6, 12.0: rch_array_12},
61+
time_series_namerecord="rcharray",
62+
interpolation_methodrecord="stepwise",
63+
)
64+
65+
sim.write_simulation()
66+
sim2 = flopy.mf6.MFSimulation.load(sim_ws=str(sim_ws))
67+
model2 = sim2.get_model("model")
68+
rch2 = model2.get_package("rcha")
69+
tas2 = rch2.get_package("tas")
70+
71+
active_keys = tas2.tas_array.get_active_key_list()
72+
keys = [key for key, _ in active_keys]
73+
assert len(active_keys) == 3
74+
assert keys == [0.0, 6.0, 12.0]
75+
76+
data_0 = tas2.tas_array.get_data(0.0)
77+
data_6 = tas2.tas_array.get_data(6.0)
78+
data_12 = tas2.tas_array.get_data(12.0)
79+
80+
assert np.allclose(data_0.max(), 0.01)
81+
assert np.allclose(data_6.max(), 0.02)
82+
assert np.allclose(data_12.max(), 0.03)
83+
84+
array_data = tas2.tas_array.array
85+
assert array_data.shape[0] == 3
86+
assert np.allclose(array_data[0].max(), 0.01)
87+
assert np.allclose(array_data[1].max(), 0.02)
88+
assert np.allclose(array_data[2].max(), 0.03)
89+
90+
91+
def test_tas_set_all_data_external(function_tmpdir):
92+
sim_ws = Path(function_tmpdir) / "tas_external_test"
93+
sim_ws.mkdir(exist_ok=True)
94+
95+
sim = flopy.mf6.MFSimulation(sim_name="tas_ext", version="mf6", sim_ws=str(sim_ws))
96+
tdis = flopy.mf6.ModflowTdis(
97+
sim,
98+
time_units="DAYS",
99+
nper=2,
100+
perioddata=[(10.0, 10, 1.0), (10.0, 10, 1.0)],
101+
)
102+
model = flopy.mf6.ModflowGwf(sim, modelname="model")
103+
ims = flopy.mf6.ModflowIms(sim)
104+
sim.register_ims_package(ims, [model.name])
105+
dis = flopy.mf6.ModflowGwfdis(
106+
model,
107+
nlay=1,
108+
nrow=5,
109+
ncol=5,
110+
delr=100.0,
111+
delc=100.0,
112+
top=0.0,
113+
botm=-10.0,
114+
)
115+
ic = flopy.mf6.ModflowGwfic(model, strt=0.0)
116+
npf = flopy.mf6.ModflowGwfnpf(model, k=10.0)
117+
oc = flopy.mf6.ModflowGwfoc(model)
118+
rch = flopy.mf6.ModflowGwfrcha(model, recharge="TIMEARRAYSERIES rcharray")
119+
tas = flopy.mf6.ModflowUtltas(
120+
rch,
121+
filename="model.rch.tas",
122+
tas_array={0.0: 0.001 * np.ones((5, 5)), 10.0: 0.002 * np.ones((5, 5))},
123+
time_series_namerecord="rcharray",
124+
interpolation_methodrecord="stepwise",
125+
)
126+
127+
sim.set_all_data_external()
128+
sim.write_simulation()
129+
130+
tas_file = sim_ws / "model.rch.tas"
131+
assert tas_file.exists()
132+
133+
with open(tas_file, "r") as f:
134+
content = f.read()
135+
assert "OPEN/CLOSE" in content
136+
assert "INTERNAL" not in content
137+
138+
external_files = list(sim_ws.glob("*.txt"))
139+
assert len(external_files) >= 2

flopy/mf6/data/mfdataarray.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -616,9 +616,12 @@ def store_as_external_file(
616616
if isinstance(data, str) and self._tas_info(data)[0] is not None:
617617
# data must not be time array series information
618618
continue
619+
# Check if dimensions are well-defined. For arrays with unknown
620+
# shape (e.g., TAS arrays with shape="(unknown)"), allow external
621+
# storage if we have actual numpy array data at runtime
619622
if storage.get_data_dimensions(current_layer)[0] == -9999:
620-
# data must have well defined dimensions to make external
621-
continue
623+
if not isinstance(data, np.ndarray):
624+
continue
622625
try:
623626
# store layer's data in external file
624627
if (
@@ -1852,33 +1855,61 @@ def _get_array(self, num_sp, apply_mult, **kwargs):
18521855
"""
18531856
data = None
18541857
output = None
1855-
for sp in range(0, num_sp):
1856-
if sp in self._data_storage:
1857-
self.get_data_prep(sp)
1858+
1859+
# Check if we have float-type keys (e.g., TAS data with time values)
1860+
# vs. integer-type stress period keys
1861+
if self._data_storage:
1862+
has_float_keys = any(
1863+
isinstance(key, float)
1864+
for key in self._data_storage.keys()
1865+
)
1866+
else:
1867+
has_float_keys = False
1868+
1869+
if has_float_keys:
1870+
# For TAS-style data with floating-point time keys,
1871+
# iterate through actual keys in sorted order
1872+
sorted_keys = sorted(self._data_storage.keys())
1873+
for key in sorted_keys:
1874+
self.get_data_prep(key)
18581875
data = super().get_data(apply_mult=apply_mult, **kwargs)
18591876
data = np.expand_dims(data, 0)
1860-
else:
1861-
# if there is no previous data provide array of
1862-
# zeros, otherwise provide last array of data found
1863-
if data is None:
1864-
# get any data
1865-
data_dimensions = None
1866-
for key in self._data_storage.keys():
1867-
self.get_data_prep(key)
1868-
data = super().get_data(
1869-
apply_mult=apply_mult, **kwargs
1870-
)
1871-
break
1872-
if data is not None:
1873-
if self.structure.type == DatumType.integer:
1874-
data = np.full_like(data, 1)
1875-
else:
1876-
data = np.full_like(data, 0.0)
1877-
data = np.expand_dims(data, 0)
1878-
if output is None or data is None:
1879-
output = data
1880-
else:
1881-
output = np.concatenate((output, data))
1877+
1878+
if output is None:
1879+
output = data
1880+
else:
1881+
output = np.concatenate((output, data))
1882+
else:
1883+
# For standard stress period data with integer keys,
1884+
# iterate through stress periods and fill missing ones
1885+
for sp in range(0, num_sp):
1886+
if sp in self._data_storage:
1887+
self.get_data_prep(sp)
1888+
data = super().get_data(apply_mult=apply_mult, **kwargs)
1889+
data = np.expand_dims(data, 0)
1890+
else:
1891+
# if there is no previous data provide array of
1892+
# zeros, otherwise provide last array of data found
1893+
if data is None:
1894+
# get any data
1895+
data_dimensions = None
1896+
for key in self._data_storage.keys():
1897+
self.get_data_prep(key)
1898+
data = super().get_data(
1899+
apply_mult=apply_mult, **kwargs
1900+
)
1901+
break
1902+
if data is not None:
1903+
if self.structure.type == DatumType.integer:
1904+
data = np.full_like(data, 1)
1905+
else:
1906+
data = np.full_like(data, 0.0)
1907+
data = np.expand_dims(data, 0)
1908+
if output is None or data is None:
1909+
output = data
1910+
else:
1911+
output = np.concatenate((output, data))
1912+
18821913
return output
18831914

18841915
def has_data(self, layer=None):

0 commit comments

Comments
 (0)