Skip to content

Commit 13c5bc6

Browse files
committed
add gwf-lak
1 parent 3d5eec4 commit 13c5bc6

35 files changed

Lines changed: 3262 additions & 204 deletions

docs/examples/frenchman-flat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ def plot_head_ugrid(head, cbc, grid, workspace):
661661
budget_file=Path("ff.cbc"),
662662
head_file=Path("ff.hds"),
663663
save_head={"0": "all", 1: "all"},
664-
save_budget={"0": "STEPS 1", 1: ""},
664+
save_budget={"0": "STEPS 1"},
665665
print_budget={"0": "STEPS 1 15", 1: "last"},
666666
dims=dims,
667667
)

flopy4/mf6/codec/writer/filters.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,13 @@ def dataset2list(value: xr.Dataset):
281281
row2.append(kw if isinstance(kw, str) else str(name).upper())
282282
else:
283283
row2.extend(da.attrs.get("prefix", ()))
284-
row2.append(val)
284+
if da.attrs.get("cellid"):
285+
if isinstance(val, tuple):
286+
row2.extend(c + 1 for c in val)
287+
else:
288+
row2.append(val + 1)
289+
else:
290+
row2.append(val)
285291
if has_spatial_dims:
286292
cellid = tuple(idx[i] + 1 for idx in indices)
287293
yield tuple(cellid) + tuple(row2)

flopy4/mf6/converter/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
from flopy4.mf6.converter.egress.unstructure import (
1111
unstructure_component,
1212
)
13-
from flopy4.mf6.converter.ingress.structure import structure_array, structure_keyword
13+
from flopy4.mf6.converter.ingress.structure import (
14+
structure_array,
15+
structure_component,
16+
structure_keyword,
17+
)
1418

1519
__all__ = [
1620
"structure",
1721
"unstructure",
1822
"structure_array",
23+
"structure_component",
1924
"unstructure_array",
2025
"structure_keyword",
2126
"COMPONENT_CONVERTER",

flopy4/mf6/converter/egress/unstructure.py

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,68 @@ def _hack_structured_grid_dims(
115115
)
116116

117117

118+
_OC_SETTING_KEYWORDS = frozenset({"all", "first", "last", "steps", "frequency"})
119+
120+
121+
def _is_emb_fill(val) -> bool:
122+
"""Return True if val is a fill/absent value for embedded keystring rows."""
123+
if val is None:
124+
return True
125+
try:
126+
f = float(val)
127+
return f == FILL_DNODATA or np.isnan(f)
128+
except (TypeError, ValueError):
129+
return False
130+
131+
132+
def _accumulate_embedded_keystring(
133+
field_value: xr.DataArray,
134+
field_meta: dict,
135+
period_data: dict,
136+
) -> None:
137+
"""Collect (ifno, keyword, value) rows per kper from a 2D embedded keystring field."""
138+
keyword = field_meta["keyword"]
139+
feature_dim = next((d for d in field_value.dims if d != "nper"), None)
140+
if feature_dim is None:
141+
return
142+
rows_by_kper: dict = period_data.setdefault("__embedded_rows__", {})
143+
for kper in range(field_value.sizes["nper"]):
144+
kper_slice = field_value.isel(nper=kper).values
145+
for ifeat, val in enumerate(kper_slice):
146+
if not _is_emb_fill(val):
147+
rows_by_kper.setdefault(kper, []).append((ifeat + 1, keyword, val))
148+
149+
150+
def _unstructure_period_keystring(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]:
151+
"""Unstructure a keystring period field (e.g. save_head, print_budget).
152+
153+
The field name encodes the MF6 keyword: save_head → 'save head'.
154+
Values are ocsetting strings such as 'ALL', 'LAST', 'STEPS 1 3', 'FREQUENCY 2'.
155+
Empty strings act as a stop sentinel — they suppress fill-forward for that period.
156+
"""
157+
fname = name.replace("_", " ")
158+
dat = {
159+
kper: value.values[kper]
160+
for kper in range(value.sizes["nper"])
161+
if (tokens := value.values[kper].lower().split()) and tokens[0] in _OC_SETTING_KEYWORDS
162+
}
163+
return {fname: dat}
164+
165+
166+
def _unstructure_period_bool(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]:
167+
"""Unstructure a boolean period field (e.g. STO steady_state, transient)."""
168+
fname = name.rstrip("_").replace("_", "-") # type: ignore
169+
dat = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]}
170+
return {fname: dat}
171+
172+
118173
def _hack_period_non_numeric(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]:
119-
data = {}
120174
match value.dtype:
121175
case np.bool:
122-
# supports boolean dataarrays, e.g. STO steady_state and transient
123-
# Strip trailing _ first (safe_name adds _ for Python builtins, e.g. all_ → all)
124-
fname = name.rstrip("_").replace("_", "-") # type: ignore
125-
dat = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]}
126-
data[fname] = dat
176+
return _unstructure_period_bool(name, value)
127177
case np.dtypes.StringDType():
128-
# supports string dataarrays, e.g. OC save_budget, save_head
129-
fname = name.replace("_", " ")
130-
dat = {
131-
kper: value.values[kper]
132-
for kper in range(value.sizes["nper"])
133-
if (tokens := value.values[kper].lower().split())
134-
and tokens[0] in ["first", "last", "steps", "all"]
135-
}
136-
data[fname] = dat
137-
138-
return data
178+
return _unstructure_period_keystring(name, value)
179+
return {}
139180

140181

141182
def _unstructure_block_param(
@@ -229,6 +270,11 @@ def _unstructure_block_param(
229270
structured_grid_dims=value.data.dims, # type: ignore
230271
)
231272
if "nper" in field_value.dims and block_name == "period":
273+
arr_spec = xatspec.arrays.get(field_name)
274+
_field_meta = (arr_spec.metadata or {}) if arr_spec is not None else {}
275+
if _field_meta.get("embedded_keystring"):
276+
_accumulate_embedded_keystring(field_value, _field_meta, period_data)
277+
return
232278
is_tabular = (
233279
np.issubdtype(field_value.dtype, np.number)
234280
or np.issubdtype(field_value.dtype, np.str_)
@@ -250,12 +296,14 @@ def _unstructure_block_param(
250296
else:
251297
arr_spec = xatspec.arrays.get(field_name)
252298
field_meta = (arr_spec.metadata or {}) if arr_spec is not None else {}
253-
if "prefix" in field_meta or "row_keyword" in field_meta:
299+
if "prefix" in field_meta or "row_keyword" in field_meta or "cellid" in field_meta:
254300
field_value = field_value.copy()
255301
if "prefix" in field_meta:
256302
field_value.attrs["prefix"] = field_meta["prefix"]
257303
if "row_keyword" in field_meta:
258304
field_value.attrs["row_keyword"] = field_meta["row_keyword"]
305+
if "cellid" in field_meta:
306+
field_value.attrs["cellid"] = True
259307
blocks[block_name][field_name] = field_value
260308
case _:
261309
blocks[block_name][field_name] = field_value
@@ -314,16 +362,29 @@ def _unstructure_array_component(value: Component) -> dict[str, Any]:
314362

315363
# Block names that MF6 rejects if present but empty.
316364
# These blocks should only be written when they contain data.
317-
_SKIP_IF_EMPTY = frozenset({"dimensions", "fileinput", "tracktimes"})
365+
_SKIP_IF_EMPTY = frozenset({"dimensions", "fileinput", "tables", "outlets", "tracktimes"})
318366

319367
# Block names whose fields are list columns (one array per column, same dim)
320368
# rather than independent grid arrays. Only these blocks are auto-combined
321369
# into an xr.Dataset for row-per-record output. griddata-style blocks must
322370
# NOT be in this set — their fields are written individually with
323371
# INTERNAL/CONSTANT/NETCDF format.
372+
# Extend when adding a new recarray block; keep in sync with __block_col_maps__
373+
# on generated Package classes.
324374
# "sources" is the SSM sources block (pname/srctype/auxname per-row tabular input).
325375
_LIST_BLOCK_NAMES = frozenset(
326-
{"packagedata", "packages", "perioddata", "sources", "fileinput", "table"}
376+
{
377+
"packagedata",
378+
"packages",
379+
"partitions",
380+
"perioddata",
381+
"sources",
382+
"fileinput",
383+
"table",
384+
"outlets",
385+
"connectiondata",
386+
"tables",
387+
}
327388
)
328389

329390

@@ -368,7 +429,15 @@ def _unstructure_component(value: Component) -> dict[str, Any]:
368429
for kper, block in period_blocks.items():
369430
key = f"period {kper + 1}"
370431
for arr_name, val in block.items():
371-
if np.any(val != FILL_DNODATA):
432+
if arr_name == "__embedded_rows__":
433+
# Embedded keystring rows: list of (ifno, keyword, value) tuples.
434+
# Accumulated from all embedded_keystring fields; write as a list
435+
# so the Jinja 'list' macro renders each tuple as a record row.
436+
if val:
437+
if key not in blocks:
438+
blocks[key] = {}
439+
blocks[key]["lak_period"] = val
440+
elif np.any(val != FILL_DNODATA):
372441
# don't create the block (so it isn't written)
373442
# unless there is data to write
374443
if key not in blocks:

0 commit comments

Comments
 (0)