Skip to content

Commit e6951c4

Browse files
authored
feat(binaryfile): add write methods (#2722)
Support writing binary head, budget and grid files, both brand-new via classmethods, and copying existing files to new paths via instance methods. Add write() classmethods to HeadFile and CellBudgetFile. These write a new file with the given data and return an instance with it open. There are a few syntax variants for head files. # dict keyed by (kstp, kper) hds = HeadFile.write( 'output.hds', # totim/pertim inferred (dt = 1.0 / time step) data={ (1, 1): h_t1, (1, 2): h_t2, } ) # list hds = HeadFile.write('output.hds', data=[ {'data': h_t2, 'kstp': 1, 'kper': 1, 'totim': 10.0, 'pertim': 10.0}, {'data': h_t2, 'kstp': 1, 'kper': 2, 'totim': 20.0, 'pertim': 10.0}, ]) # array/list with time as first dimension # defaults to sequential stress periods: (1,1), (1,2), (1,3), ... heads = [h_t1, h_t2, h_t3] # or np.array([h_t1, h_t2, h_t3]) hds = HeadFile.write('output.hds', heads) # array/list with custom tdis hds = HeadFile.write('output.hds', heads, kstpkper=[(1, 1), (2, 1), (3, 1)]) Again there are variants for budget files. The typical case is to use data with a list, where each entry has "text", "kper", "kstp", and "data" entries, and "data" is an array, typically grid-shaped. But it can be convenient to use text and pass a time-indexed dictionary to data to create a file with a single variable. # just face flows, dict keyed by (kstp, kper) cbc = CellBudgetFile.write( 'output.cbc', text='FLOW-JA-FACE', nlay=3, nrow=10, ncol=20, data={ (1, 1): q_t1, (1, 2): q_t2, } ) # multiple variables, list cbc = CellBudgetFile.write( 'output.cbc', data=[ {'data': q_t1, 'kstp': 1, 'kper': 1, 'totim': 10.0, 'text': 'FLOW-JA-FACE'}, {'data': ...}, ... ] ) # array/list with time dimension (grid-shaped data like storage) # defaults to sequential stress periods: (1,1), (1,2), (1,3), ... # grid dimensions inferred from array shape storage = [s_t1, s_t2, s_t3] # nlay x nrow x ncol arrays cbc = CellBudgetFile.write('output.cbc', storage, text='STORAGE') # for face flows, grid dimensions are required since data is 1D flows = [q_t1, q_t2] # 1D arrays cbc = CellBudgetFile.write( 'output.cbc', flows, text='FLOW-JA-FACE', nlay=3, nrow=10, ncol=20 ) If only face flows are provided, the grid shape must be specified with nlay/nrow/ncol, nlay/ncpl, or nnodes. If grid-shaped variables are provided, the grid's shape will be inferred. Instance methods Add instance export() methods to MfGrdFile, HeadFile, and CellBudgetFile. These copy the contents of an open file to another, optionally filtering by variable and/or time step, or changing the precision. There is no write() method for MfGrdFile, as its signature would have been long and complicated to accommodate all grid types. from flopy.mf6.utils.binarygrid_util import MfGrdFile grb = MfGrdFile("model.grb") grb.export("copy.grb") # copy to another file grb.export("diff_prec.grb", precision="single") # different precision from flopy.utils.binaryfile import HeadFile, CellBudgetFile hds = HeadFile("model.hds") hds.export("copy.hds") # copy to another file hds.export("filtered.hds", kstpkper=[(1, 0), (1, 1)]) # filter time steps hds.export("diff_prec.hds", precision="single") # different precision cbc = CellBudgetFile("model.cbc") cbc.export("copy.cbc") cbc.export("flowja.cbc", text="FLOW-JA-FACE") cbc.export("bndpkgs.cbc", text=["STORAGE", "CONSTANT HEAD"]) cbc.export("filtered.cbc", kstpkper=[(1, 0)], text="FLOW-JA-FACE")
1 parent cbc6e86 commit e6951c4

7 files changed

Lines changed: 2643 additions & 4 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Benchmark tests for binaryfile write methods."""
2+
3+
from pathlib import Path
4+
5+
import numpy as np
6+
import pytest
7+
8+
from flopy.mf6.utils import MfGrdFile
9+
from flopy.utils import CellBudgetFile, HeadFile
10+
11+
12+
@pytest.fixture
13+
def freyberg_hds_path(example_data_path):
14+
return example_data_path / "freyberg_multilayer_transient" / "freyberg.hds"
15+
16+
17+
@pytest.fixture
18+
def freyberg_cbc_path(example_data_path):
19+
return example_data_path / "freyberg_multilayer_transient" / "freyberg.cbc"
20+
21+
22+
@pytest.fixture
23+
def mfgrd_dis_path(example_data_path):
24+
return example_data_path / "mf6-freyberg" / "freyberg.dis.grb"
25+
26+
27+
@pytest.fixture
28+
def mfgrd_disv_path(example_data_path):
29+
return (
30+
example_data_path
31+
/ "mf6"
32+
/ "test006_gwf3_disv"
33+
/ "expected_output"
34+
/ "flow.disv.grb"
35+
)
36+
37+
38+
@pytest.mark.slow
39+
def test_headfile_write_benchmark(benchmark, freyberg_hds_path, tmp_path):
40+
hds = HeadFile(freyberg_hds_path)
41+
nsteps = min(100, len(hds.kstpkper))
42+
kstpkper = hds.kstpkper[:nsteps]
43+
output_file = tmp_path / "benchmark_output.hds"
44+
45+
def write_head():
46+
hds.export(output_file, kstpkper=kstpkper)
47+
48+
benchmark(write_head)
49+
assert output_file.exists()
50+
51+
52+
@pytest.mark.slow
53+
def test_cellbudgetfile_write_benchmark(benchmark, freyberg_cbc_path, tmp_path):
54+
cbc = CellBudgetFile(freyberg_cbc_path)
55+
nsteps = min(50, len(cbc.kstpkper))
56+
kstpkper = cbc.kstpkper[:nsteps]
57+
output_file = tmp_path / "benchmark_output.cbc"
58+
59+
def write_budget():
60+
cbc.export(output_file, kstpkper=kstpkper)
61+
62+
benchmark(write_budget)
63+
assert output_file.exists()
64+
65+
66+
@pytest.mark.slow
67+
def test_mfgrdfile_write_benchmark_dis(benchmark, tmp_path):
68+
nlay, nrow, ncol = 10, 100, 100
69+
nodes = nlay * nrow * ncol
70+
nja = 7 * nodes - 2 * (nlay * nrow + nlay * ncol + nrow * ncol)
71+
grb_data = {
72+
"NCELLS": nodes,
73+
"NLAY": nlay,
74+
"NROW": nrow,
75+
"NCOL": ncol,
76+
"NJA": nja,
77+
"XORIGIN": 0.0,
78+
"YORIGIN": 0.0,
79+
"ANGROT": 0.0,
80+
"DELR": np.ones(ncol, dtype=np.float64) * 100.0,
81+
"DELC": np.ones(nrow, dtype=np.float64) * 100.0,
82+
"TOP": np.ones(nodes, dtype=np.float64) * 100.0,
83+
"BOTM": np.arange(nodes, dtype=np.float64),
84+
"IA": np.arange(nodes + 1, dtype=np.int32),
85+
"JA": np.arange(nja, dtype=np.int32) % nodes,
86+
"IDOMAIN": np.ones(nodes, dtype=np.int32),
87+
"ICELLTYPE": np.ones(nodes, dtype=np.int32),
88+
}
89+
90+
from flopy.utils.utils_def import FlopyBinaryData
91+
92+
temp_grb = tmp_path / "temp_input.grb"
93+
writer = FlopyBinaryData()
94+
writer.precision = "double"
95+
96+
with open(temp_grb, "wb") as f:
97+
writer.file = f
98+
writer.write_text("GRID DIS\n", 50)
99+
writer.write_text("VERSION 1\n", 50)
100+
writer.write_text("NTXT 16\n", 50)
101+
writer.write_text("LENTXT 100\n", 50)
102+
var_list = [
103+
("NCELLS", "INTEGER", 0, []),
104+
("NLAY", "INTEGER", 0, []),
105+
("NROW", "INTEGER", 0, []),
106+
("NCOL", "INTEGER", 0, []),
107+
("NJA", "INTEGER", 0, []),
108+
("XORIGIN", "DOUBLE", 0, []),
109+
("YORIGIN", "DOUBLE", 0, []),
110+
("ANGROT", "DOUBLE", 0, []),
111+
("DELR", "DOUBLE", 1, [ncol]),
112+
("DELC", "DOUBLE", 1, [nrow]),
113+
("TOP", "DOUBLE", 1, [nodes]),
114+
("BOTM", "DOUBLE", 1, [nodes]),
115+
("IA", "INTEGER", 1, [nodes + 1]),
116+
("JA", "INTEGER", 1, [nja]),
117+
("IDOMAIN", "INTEGER", 1, [nodes]),
118+
("ICELLTYPE", "INTEGER", 1, [nodes]),
119+
]
120+
121+
for name, dtype_str, ndim, dims in var_list:
122+
if ndim == 0:
123+
line = f"{name} {dtype_str} NDIM {ndim}\n"
124+
else:
125+
dims_str = " ".join(str(d) for d in dims[::-1])
126+
line = f"{name} {dtype_str} NDIM {ndim} {dims_str}\n"
127+
writer.write_text(line, 100)
128+
129+
for name, dtype_str, ndim, dims in var_list:
130+
value = grb_data[name]
131+
if ndim == 0:
132+
if dtype_str == "INTEGER":
133+
writer.write_integer(int(value))
134+
else:
135+
writer.write_real(float(value))
136+
else:
137+
arr = np.asarray(value)
138+
if dtype_str == "INTEGER":
139+
arr = arr.astype(np.int32)
140+
elif dtype_str == "DOUBLE":
141+
arr = arr.astype(np.float64)
142+
writer.write_record(arr.flatten(order="F"), dtype=arr.dtype)
143+
144+
grb = MfGrdFile(str(temp_grb), verbose=False)
145+
output_file = tmp_path / "benchmark_output.grb"
146+
147+
def write_grb():
148+
grb.export(output_file, verbose=False)
149+
150+
benchmark(write_grb)
151+
assert output_file.exists()

0 commit comments

Comments
 (0)