Skip to content

Commit 3a60459

Browse files
feat(arrays): add model.free_format_npl to control values-per-line in free-format array output
When free_format_npl is set (e.g., model.free_format_npl = 10), free-format arrays are written with that many values per line instead of all values on a single line. This produces block-format output matching Groundwater Vistas style, improving readability for large unstructured models. Affects both INTERNAL arrays (Util2d.string) and EXTERNAL/OPENCLOSE arrays (Util2d.get_file_entry). Default is None (no change in behavior).
1 parent ae1533f commit 3a60459

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

autotest/test_usg.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,70 @@ def test_load_usg(function_tmpdir, fpth):
404404

405405
m.change_model_ws(function_tmpdir)
406406
m.write_input()
407+
408+
409+
def test_free_format_npl(function_tmpdir, freyberg_usg_model_path):
410+
"""Test that free_format_npl controls values per line in array output."""
411+
nam = "freyberg.usg.nam"
412+
413+
# Load model with free_format_npl=10
414+
m = MfUsg.load(nam, model_ws=freyberg_usg_model_path)
415+
m.free_format_npl = 10
416+
m.model_ws = function_tmpdir
417+
m.write_input()
418+
419+
# Read the written RCH file and check values per line
420+
rch_file = function_tmpdir / f"{m.name}.rch"
421+
assert rch_file.is_file()
422+
423+
with open(rch_file) as f:
424+
lines = f.readlines()
425+
426+
# Find a data line (not a header/control record) with multiple values
427+
for line in lines:
428+
parts = line.strip().split()
429+
if len(parts) >= 5:
430+
try:
431+
[float(p) for p in parts]
432+
# This is a data line — verify it has at most 10 values
433+
assert len(parts) <= 10, (
434+
f"Expected at most 10 values per line, got {len(parts)}"
435+
)
436+
break
437+
except ValueError:
438+
continue
439+
440+
# Also verify default behavior (npl=None) writes all values on one line
441+
m2 = MfUsg.load(nam, model_ws=freyberg_usg_model_path)
442+
assert m2.free_format_npl is None
443+
out2 = function_tmpdir / "default"
444+
out2.mkdir()
445+
m2.model_ws = out2
446+
m2.write_input()
447+
448+
rch_file2 = out2 / f"{m2.name}.rch"
449+
with open(rch_file2) as f:
450+
lines2 = f.readlines()
451+
452+
# Find a data line — with default npl it should have more than 10 values
453+
for line in lines2:
454+
parts = line.strip().split()
455+
if len(parts) >= 5:
456+
try:
457+
[float(p) for p in parts]
458+
assert len(parts) > 10, (
459+
f"Expected more than 10 values per line with default npl, "
460+
f"got {len(parts)}"
461+
)
462+
break
463+
except ValueError:
464+
continue
465+
466+
467+
def test_free_format_npl_constructor():
468+
"""Test that free_format_npl can be set via constructor kwarg."""
469+
m = MfUsg(free_format_npl=10)
470+
assert m.free_format_npl == 10
471+
472+
m2 = MfUsg()
473+
assert m2.free_format_npl is None

flopy/mbase.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ class BaseModel(ModelInterface):
375375
Specify if model grid is structured (default) or unstructured.
376376
verbose : bool, default False
377377
Print additional information to the screen.
378+
free_format_npl : int, optional
379+
Number of values per line when writing free-format arrays. When set
380+
(e.g., ``free_format_npl=10``), arrays are written with that many
381+
values per line instead of all values on a single line. This produces
382+
block-format output matching Groundwater Vistas style, improving
383+
readability for large models. Default is None (all values on one line).
378384
**kwargs : dict, optional
379385
Used to define: ``xll``/``yll`` for the x- and y-coordinates of
380386
the lower-left corner of the grid, ``xul``/``yul`` for the
@@ -393,6 +399,7 @@ def __init__(
393399
model_ws: Union[str, PathLike] = curdir,
394400
structured=True,
395401
verbose=False,
402+
free_format_npl=None,
396403
**kwargs,
397404
):
398405
"""Initialize BaseModel."""
@@ -453,6 +460,7 @@ def __init__(
453460
# external option stuff
454461
self.array_free_format = True
455462
self.free_format_input = True
463+
self.free_format_npl = free_format_npl
456464
self.parameter_load = False
457465
self.array_format = None
458466
self.external_fnames = []

flopy/utils/util_array.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2475,11 +2475,23 @@ def get_file_entry(self, how=None):
24752475
self.shape, self.python_file_path, self._array, bintype="head"
24762476
)
24772477
else:
2478+
# Override npl for free format if model specifies free_format_npl
2479+
python_format = None
2480+
if (
2481+
self.format.free
2482+
and self._model is not None
2483+
and getattr(self._model, "free_format_npl", None) is not None
2484+
):
2485+
python_format = (
2486+
self._model.free_format_npl,
2487+
self.format.py[1],
2488+
)
24782489
self.write_txt(
24792490
self.shape,
24802491
self.python_file_path,
24812492
self._array,
24822493
fortran_format=self.format.fortran,
2494+
python_format=python_format,
24832495
)
24842496

24852497
elif self.__value != self.python_file_path:
@@ -2536,8 +2548,16 @@ def string(self):
25362548
25372549
"""
25382550
# convert array to string with specified format
2551+
python_format = self.format.py
2552+
# Override npl for free format if model specifies free_format_npl
2553+
if (
2554+
self.format.free
2555+
and self._model is not None
2556+
and getattr(self._model, "free_format_npl", None) is not None
2557+
):
2558+
python_format = (self._model.free_format_npl, python_format[1])
25392559
a_string = self.array2string(
2540-
self.shape, self._array, python_format=self.format.py
2560+
self.shape, self._array, python_format=python_format
25412561
)
25422562
return a_string
25432563

0 commit comments

Comments
 (0)