Skip to content

Commit 7554dff

Browse files
feat(mfusgwel): add comment column support for MfUsgWel package
Captures undeclared trailing text in MODFLOW-USG WEL files (common in Groundwater Vistas exports) as comment columns with object dtype. Comments are preserved on read/write roundtrip without being declared as AUX in the header.
1 parent ae1533f commit 7554dff

4 files changed

Lines changed: 163 additions & 7 deletions

File tree

flopy/mfusg/mfusgwel.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ def __init__(
239239
self.parent.add_package(self)
240240

241241
@staticmethod
242-
def get_empty(ncells=0, aux_names=None, structured=True, wellbot=False):
242+
def get_empty(
243+
ncells=0, aux_names=None, structured=True, wellbot=False, n_comments=0
244+
):
243245
"""Get empty recarray for MFUSG wells.
244246
245247
Parameters
@@ -252,6 +254,8 @@ def get_empty(ncells=0, aux_names=None, structured=True, wellbot=False):
252254
Whether grid is structured
253255
wellbot : bool
254256
Whether WELLBOT option is used
257+
n_comments : int
258+
Number of comment columns to add (default is 0)
255259
256260
Returns
257261
-------
@@ -275,6 +279,12 @@ def get_empty(ncells=0, aux_names=None, structured=True, wellbot=False):
275279
if aux_names is not None:
276280
dtype = Package.add_to_dtype(dtype, aux_names, np.float32)
277281

282+
# Add comment columns
283+
if n_comments > 0:
284+
comment_names = [f"comment{i + 1}" for i in range(n_comments)]
285+
for name in comment_names:
286+
dtype = Package.add_to_dtype(dtype, name, object)
287+
278288
return create_empty_recarray(ncells, dtype, default_value=-1.0e10)
279289

280290
def _check_for_aux(self, options, cln=False):
@@ -297,6 +307,9 @@ def _check_for_aux(self, options, cln=False):
297307
dt = self.get_default_dtype(structured=self.parent.structured)
298308
if len(self.dtype.names) > len(dt.names):
299309
for name in self.dtype.names[len(dt.names) :]:
310+
# Skip comment columns (object dtype) — not AUX variables
311+
if self.dtype.fields[name][0] == object:
312+
continue
300313
ladd = True
301314
for option in options:
302315
if name.lower() in option.lower():

flopy/modflow/mfwel.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ def __init__(
199199
dt = self.get_default_dtype(structured=self.parent.structured)
200200
if len(self.dtype.names) > len(dt.names):
201201
for name in self.dtype.names[len(dt.names) :]:
202+
# Skip comment columns (object dtype) — not AUX variables
203+
if self.dtype.fields[name][0] == object:
204+
continue
202205
ladd = True
203206
for option in options:
204207
if name.lower() in option.lower():

flopy/pakbase.py

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
import numpy as np
1616
from numpy.lib.recfunctions import stack_arrays
1717

18-
from .utils import MfList, OptionBlock, Transient2d, Util2d, Util3d, check
18+
from .utils import (
19+
MfList,
20+
OptionBlock,
21+
Transient2d,
22+
Util2d,
23+
Util3d,
24+
check,
25+
create_empty_recarray,
26+
)
1927
from .utils.flopy_io import ulstrd
2028

2129

@@ -1085,6 +1093,9 @@ def load(
10851093
bnd_output_cln = None
10861094
stress_period_data_cln = {}
10871095
current_cln = None
1096+
is_mfusgwel = "mfusgwel" in pak_type_str
1097+
max_comment_cols = 0
1098+
max_comment_cols_cln = 0
10881099
for iper in range(nper):
10891100
if model.verbose:
10901101
msg = f" loading {pak_type} for kper {iper + 1:5d}"
@@ -1126,7 +1137,41 @@ def load(
11261137
current = pak_type.get_empty(
11271138
itmp, aux_names=aux_names, structured=model.structured, **usg_args
11281139
)
1129-
current = ulstrd(f, itmp, current, model, sfac_columns, ext_unit_dict)
1140+
if is_mfusgwel:
1141+
current, extra_tokens = ulstrd(
1142+
f,
1143+
itmp,
1144+
current,
1145+
model,
1146+
sfac_columns,
1147+
ext_unit_dict,
1148+
capture_comments=True,
1149+
)
1150+
n_comments = max((len(ct) for ct in extra_tokens), default=0)
1151+
if n_comments > 0:
1152+
max_comment_cols = max(max_comment_cols, n_comments)
1153+
new_dtype = current.dtype
1154+
for ci in range(n_comments):
1155+
new_dtype = Package.add_to_dtype(
1156+
new_dtype, f"comment{ci + 1}", object
1157+
)
1158+
new_ra = create_empty_recarray(
1159+
len(current), new_dtype, default_value=-1.0e10
1160+
)
1161+
for name in current.dtype.names:
1162+
new_ra[name] = current[name]
1163+
for ii in range(len(current)):
1164+
for ci in range(n_comments):
1165+
cname = f"comment{ci + 1}"
1166+
if ci < len(extra_tokens[ii]):
1167+
new_ra[cname][ii] = extra_tokens[ii][ci]
1168+
else:
1169+
new_ra[cname][ii] = ""
1170+
current = new_ra
1171+
else:
1172+
current = ulstrd(
1173+
f, itmp, current, model, sfac_columns, ext_unit_dict
1174+
)
11301175
if model.structured:
11311176
current["k"] -= 1
11321177
current["i"] -= 1
@@ -1149,9 +1194,48 @@ def load(
11491194
current_cln = pak_type.get_empty(
11501195
itmp_cln, aux_names=aux_names, structured=False, **usg_args
11511196
)
1152-
current_cln = ulstrd(
1153-
f, itmp_cln, current_cln, model, sfac_columns, ext_unit_dict
1154-
)
1197+
if is_mfusgwel:
1198+
current_cln, extra_tokens_cln = ulstrd(
1199+
f,
1200+
itmp_cln,
1201+
current_cln,
1202+
model,
1203+
sfac_columns,
1204+
ext_unit_dict,
1205+
capture_comments=True,
1206+
)
1207+
n_comments_cln = max(
1208+
(len(ct) for ct in extra_tokens_cln), default=0
1209+
)
1210+
if n_comments_cln > 0:
1211+
max_comment_cols_cln = max(max_comment_cols_cln, n_comments_cln)
1212+
new_dtype = current_cln.dtype
1213+
for ci in range(n_comments_cln):
1214+
new_dtype = Package.add_to_dtype(
1215+
new_dtype, f"comment{ci + 1}", object
1216+
)
1217+
new_ra = create_empty_recarray(
1218+
len(current_cln), new_dtype, default_value=-1.0e10
1219+
)
1220+
for name in current_cln.dtype.names:
1221+
new_ra[name] = current_cln[name]
1222+
for ii in range(len(current_cln)):
1223+
for ci in range(n_comments_cln):
1224+
cname = f"comment{ci + 1}"
1225+
if ci < len(extra_tokens_cln[ii]):
1226+
new_ra[cname][ii] = extra_tokens_cln[ii][ci]
1227+
else:
1228+
new_ra[cname][ii] = ""
1229+
current_cln = new_ra
1230+
else:
1231+
current_cln = ulstrd(
1232+
f,
1233+
itmp_cln,
1234+
current_cln,
1235+
model,
1236+
sfac_columns,
1237+
ext_unit_dict,
1238+
)
11551239
current_cln["node"] -= 1
11561240
bnd_output_cln = np.recarray.copy(current_cln)
11571241
else:
@@ -1231,6 +1315,43 @@ def load(
12311315
0, aux_names=aux_names, structured=model.structured, **usg_args
12321316
).dtype
12331317

1318+
# Normalize comment columns across stress periods for MfUsgWel
1319+
if is_mfusgwel and (max_comment_cols > 0 or max_comment_cols_cln > 0):
1320+
for spd_dict, max_cc in [
1321+
(stress_period_data, max_comment_cols),
1322+
(stress_period_data_cln, max_comment_cols_cln),
1323+
]:
1324+
if max_cc == 0:
1325+
continue
1326+
for iper in spd_dict:
1327+
spd = spd_dict[iper]
1328+
if not isinstance(spd, np.recarray):
1329+
continue
1330+
existing = sum(
1331+
1 for n in spd.dtype.names if n.startswith("comment")
1332+
)
1333+
if existing < max_cc:
1334+
new_dtype = spd.dtype
1335+
for ci in range(existing, max_cc):
1336+
new_dtype = Package.add_to_dtype(
1337+
new_dtype, f"comment{ci + 1}", object
1338+
)
1339+
new_ra = create_empty_recarray(
1340+
len(spd), new_dtype, default_value=-1.0e10
1341+
)
1342+
for name in spd.dtype.names:
1343+
new_ra[name] = spd[name]
1344+
for ci in range(existing, max_cc):
1345+
cname = f"comment{ci + 1}"
1346+
for ii in range(len(new_ra)):
1347+
new_ra[cname][ii] = ""
1348+
spd_dict[iper] = new_ra
1349+
1350+
# Update dtype to include comment columns
1351+
if max_comment_cols > 0:
1352+
for ci in range(max_comment_cols):
1353+
dtype = Package.add_to_dtype(dtype, f"comment{ci + 1}", object)
1354+
12341355
if openfile:
12351356
f.close()
12361357

@@ -1248,6 +1369,12 @@ def load(
12481369
cln_dtype = pak_type.get_empty(
12491370
0, aux_names=aux_names, structured=False, **usg_args
12501371
).dtype
1372+
# Update cln_dtype to include comment columns
1373+
if max_comment_cols_cln > 0:
1374+
for ci in range(max_comment_cols_cln):
1375+
cln_dtype = Package.add_to_dtype(
1376+
cln_dtype, f"comment{ci + 1}", object
1377+
)
12511378
pak = pak_type(
12521379
model,
12531380
ipakcb=ipakcb,

flopy/utils/flopy_io.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def get_url_text(url, error_msg=None):
382382
return
383383

384384

385-
def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict):
385+
def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict, capture_comments=False):
386386
"""
387387
Read a list and allow for open/close, binary, external, sfac, etc.
388388
@@ -404,9 +404,17 @@ def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict):
404404
then in this case ext_unit_dict is required, which can be
405405
constructed using the function
406406
:class:`flopy.utils.mfreadnam.parsenamefile`.
407+
capture_comments : bool, optional
408+
If True, capture extra tokens beyond the expected number of columns
409+
and return them as a list of lists alongside the recarray.
410+
(default is False)
407411
408412
Returns
409413
-------
414+
ra : np.recarray
415+
The filled record array. If capture_comments is True, returns a
416+
tuple of (ra, comment_tokens) where comment_tokens is a list of
417+
lists containing extra tokens for each row.
410418
411419
"""
412420

@@ -419,6 +427,7 @@ def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict):
419427
close_the_file = False
420428
file_handle = f
421429
mode = "r"
430+
comment_tokens = [[] for _ in range(nlist)] if capture_comments else None
422431

423432
# check for external
424433
if line.strip().lower().startswith("external"):
@@ -488,6 +497,8 @@ def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict):
488497
if model.free_format_input:
489498
# whitespace separated
490499
t = line_parse(line)
500+
if capture_comments and len(t) > ncol:
501+
comment_tokens[ii] = t[ncol:]
491502
if len(t) < ncol:
492503
t = t + (ncol - len(t)) * [0.0]
493504
else:
@@ -509,6 +520,8 @@ def ulstrd(f, nlist, ra, model, sfac_columns, ext_unit_dict):
509520
if close_the_file:
510521
file_handle.close()
511522

523+
if capture_comments:
524+
return ra, comment_tokens
512525
return ra
513526

514527

0 commit comments

Comments
 (0)