From e6f88f1f25317bf6013a25dc160c22ee15ae65b6 Mon Sep 17 00:00:00 2001 From: mjreno Date: Mon, 3 Nov 2025 12:40:40 -0500 Subject: [PATCH 1/7] write storage with hack --- flopy4/mf6/converter/unstructure.py | 37 +++++++++++++++++++++++------ test/test_mf6_codec.py | 22 +++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index 4047a483..bcd1880a 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Any +import numpy as np import xarray as xr import xattree from modflow_devtools.dfn.schema.block import block_sort_key @@ -89,6 +90,16 @@ def _hack_structured_grid_dims( ) +def _hack_period_non_numeric(name, value) -> dict: + match value.dtype: + case np.bool: + data = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} + return name, data + case np.dtypes.StringDType(): + data = {kper: value.values[kper] for kper in range(value.sizes["nper"])} + return name.replace("_", " "), data + + def unstructure_component(value: Component) -> dict[str, Any]: blockspec = dict(sorted(value.dfn.blocks.items(), key=block_sort_key)) # type: ignore blocks: dict[str, dict[str, Any]] = {} @@ -157,10 +168,14 @@ def unstructure_component(value: Component) -> dict[str, Any]: structured_grid_dims=value.parent.data.dims, # type: ignore ) if block_name == "period": - period_data[field_name] = { - kper: field_value.isel(nper=kper) - for kper in range(field_value.sizes["nper"]) - } + if not np.issubdtype(field_value.dtype, np.number): + n, v = _hack_period_non_numeric(field_name, field_value) + period_data[n] = v + else: + period_data[field_name] = { + kper: field_value.isel(nper=kper) + for kper in range(field_value.sizes["nper"]) + } else: blocks[block_name][field_name] = field_value @@ -174,11 +189,19 @@ def unstructure_component(value: Component) -> dict[str, Any]: period_blocks[kper] = {} period_blocks[kper][arr_name] = arr + # sort kper order + period_blocks = dict(sorted(period_blocks.items())) + # setup indexed period blocks, combine arrays into datasets for kper, block in period_blocks.items(): - blocks[f"period {kper + 1}"] = { - "period": xr.Dataset(block, coords=block[arr_name].coords) - } + arr_name = list(block.keys())[0] + match block[arr_name]: + case str(): + blocks[f"period {kper + 1}"] = {arr_name: block[arr_name]} + case xr.DataArray(): + blocks[f"period {kper + 1}"] = { + "period": xr.Dataset(block, coords=block[arr_name].coords) + } # combine "perioddata" block arrays (tdis, ats) into datasets # so they render as lists. temp hack TODO do this generically diff --git a/test/test_mf6_codec.py b/test/test_mf6_codec.py index 55a367d7..7d17fdba 100644 --- a/test/test_mf6_codec.py +++ b/test/test_mf6_codec.py @@ -57,6 +57,28 @@ def test_dumps_ic(): pprint(loaded) +def test_dumps_sto(): + from flopy4.mf6.gwf import Dis, Gwf, Sto + + dis = Dis() + gwf = Gwf(dis=dis) + sto = Sto( + dims={"nper": 3}, + parent=gwf, + steady_state=[False, True, False], + transient=[True, False, True], + ) + + dumped = dumps(COMPONENT_CONVERTER.unstructure(sto)) + print("STO dump:") + print(dumped) + assert dumped + + loaded = loads(dumped) + print("STO load:") + pprint(loaded) + + @pytest.mark.xfail(reason="nested type unstructuring not yet supported") def test_dumps_oc(): from flopy4.mf6.gwf import Oc From c8b99e4ad7ca824c1e061d991a196ea79d747b95 Mon Sep 17 00:00:00 2001 From: mjreno Date: Mon, 3 Nov 2025 12:55:26 -0500 Subject: [PATCH 2/7] lint try 1 --- flopy4/mf6/converter/unstructure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index bcd1880a..195e31cd 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -90,7 +90,7 @@ def _hack_structured_grid_dims( ) -def _hack_period_non_numeric(name, value) -> dict: +def _hack_period_non_numeric(name: str, value: xr.DataArray) -> tuple[str, dict[int, str]]: match value.dtype: case np.bool: data = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} From 4191dbb9c3036a690e4619e8ed442741606efe39 Mon Sep 17 00:00:00 2001 From: mjreno Date: Mon, 3 Nov 2025 13:23:04 -0500 Subject: [PATCH 3/7] lint 2 --- flopy4/mf6/converter/unstructure.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index 195e31cd..5797c23c 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -91,13 +91,17 @@ def _hack_structured_grid_dims( def _hack_period_non_numeric(name: str, value: xr.DataArray) -> tuple[str, dict[int, str]]: + fname = "" + data = {} match value.dtype: case np.bool: + fname = name data = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} - return name, data case np.dtypes.StringDType(): + fname = name.replace("_", " ") data = {kper: value.values[kper] for kper in range(value.sizes["nper"])} - return name.replace("_", " "), data + + return fname, data def unstructure_component(value: Component) -> dict[str, Any]: From 3a3a0e28f79df0d6da4647127eea5bed8e577c76 Mon Sep 17 00:00:00 2001 From: mjreno Date: Tue, 4 Nov 2025 10:17:50 -0500 Subject: [PATCH 4/7] baseline oc test --- flopy4/mf6/converter/unstructure.py | 48 +++++++++++++++++++---------- test/test_mf6_codec.py | 11 +++---- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index 5797c23c..3faea219 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -90,18 +90,31 @@ def _hack_structured_grid_dims( ) -def _hack_period_non_numeric(name: str, value: xr.DataArray) -> tuple[str, dict[int, str]]: +def _hack_period_non_numeric(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]: + from flopy4.mf6.gwf import Oc + fname = "" data = {} match value.dtype: case np.bool: - fname = name - data = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} + fname = name # type: ignore + dat = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} + data[fname] = dat case np.dtypes.StringDType(): fname = name.replace("_", " ") - data = {kper: value.values[kper] for kper in range(value.sizes["nper"])} + dat = {kper: value.values[kper] for kper in range(value.sizes["nper"])} + data[fname] = dat + case object(): + if isinstance(value.values[0], Oc.PrintSaveSetting): + for rec in value.values[0].printrecord: + if rec.steps.all: + dat = {kper: "all" for kper in range(value.sizes["nper"])} + key = f"PRINT {rec.rtype}" + data[key] = dat + # for rec in value.values[0].saverecord: + # print("SaveRecord") - return fname, data + return data def unstructure_component(value: Component) -> dict[str, Any]: @@ -173,11 +186,12 @@ def unstructure_component(value: Component) -> dict[str, Any]: ) if block_name == "period": if not np.issubdtype(field_value.dtype, np.number): - n, v = _hack_period_non_numeric(field_name, field_value) - period_data[n] = v + dat = _hack_period_non_numeric(field_name, field_value) + for n, v in dat.items(): + period_data[n] = v else: period_data[field_name] = { - kper: field_value.isel(nper=kper) + kper: field_value.isel(nper=kper) # type: ignore for kper in range(field_value.sizes["nper"]) } else: @@ -195,17 +209,19 @@ def unstructure_component(value: Component) -> dict[str, Any]: # sort kper order period_blocks = dict(sorted(period_blocks.items())) + print(period_blocks) # setup indexed period blocks, combine arrays into datasets for kper, block in period_blocks.items(): - arr_name = list(block.keys())[0] - match block[arr_name]: - case str(): - blocks[f"period {kper + 1}"] = {arr_name: block[arr_name]} - case xr.DataArray(): - blocks[f"period {kper + 1}"] = { - "period": xr.Dataset(block, coords=block[arr_name].coords) - } + blocks[f"period {kper + 1}"] = {} + for arr_name, val in block.items(): + match block[arr_name]: + case str(): + blocks[f"period {kper + 1}"][arr_name] = val + case xr.DataArray(): + blocks[f"period {kper + 1}"]["period"] = xr.Dataset( + block, coords=block[arr_name].coords + ) # combine "perioddata" block arrays (tdis, ats) into datasets # so they render as lists. temp hack TODO do this generically diff --git a/test/test_mf6_codec.py b/test/test_mf6_codec.py index 7d17fdba..843ba5ed 100644 --- a/test/test_mf6_codec.py +++ b/test/test_mf6_codec.py @@ -2,8 +2,6 @@ from pprint import pprint -import pytest - from flopy4.mf6.codec import dumps, loads from flopy4.mf6.converter import COMPONENT_CONVERTER @@ -79,7 +77,6 @@ def test_dumps_sto(): pprint(loaded) -@pytest.mark.xfail(reason="nested type unstructuring not yet supported") def test_dumps_oc(): from flopy4.mf6.gwf import Oc @@ -102,10 +99,10 @@ def test_dumps_oc(): dumped = dumps(COMPONENT_CONVERTER.unstructure(oc)) print("OC dump:") print(dumped) - assert "save head all" in dumped - assert "save budget all" in dumped - assert "print head all" in dumped - assert "print budget all" in dumped + assert "SAVE HEAD all" in dumped + assert "SAVE BUDGET all" in dumped + assert "PRINT HEAD all" in dumped + assert "PRINT BUDGET all" in dumped assert dumped loaded = loads(dumped) From b4c5aab98b587fb6511906dacbac322704e03618 Mon Sep 17 00:00:00 2001 From: mjreno Date: Wed, 5 Nov 2025 09:36:49 -0500 Subject: [PATCH 5/7] support oc step types --- flopy4/mf6/converter/unstructure.py | 50 ++++++++++++++++++++++------- test/test_mf6_codec.py | 41 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index 3faea219..ead67114 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -93,26 +93,53 @@ def _hack_structured_grid_dims( def _hack_period_non_numeric(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]: from flopy4.mf6.gwf import Oc - fname = "" + def oc_setting_data(rec, action): + dat = {} + if rec.steps.first: + dat = {kper: "first" for kper in range(value.sizes["nper"])} + key = f"{action} {rec.rtype}" + data[key] = dat + elif rec.steps.last: + dat = {kper: "last" for kper in range(value.sizes["nper"])} + key = f"{action} {rec.rtype}" + data[key] = dat + elif rec.steps.steps: + steps = " ".join(str(x - 1) for x in rec.steps.steps) + dat = {kper: f"steps {steps}" for kper in range(value.sizes["nper"])} + key = f"{action} {rec.rtype}" + data[key] = dat + elif rec.steps.all: + # check last as this defaults to True + dat = {kper: "all" for kper in range(value.sizes["nper"])} + key = f"{action} {rec.rtype}" + data[key] = dat + return dat + data = {} match value.dtype: case np.bool: - fname = name # type: ignore - dat = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} - data[fname] = dat + dat = {kper: "" for kper in range(value.sizes["nper"]) if value.values[kper]} # type: ignore + data[name] = dat case np.dtypes.StringDType(): fname = name.replace("_", " ") dat = {kper: value.values[kper] for kper in range(value.sizes["nper"])} data[fname] = dat case object(): if isinstance(value.values[0], Oc.PrintSaveSetting): - for rec in value.values[0].printrecord: - if rec.steps.all: - dat = {kper: "all" for kper in range(value.sizes["nper"])} - key = f"PRINT {rec.rtype}" - data[key] = dat - # for rec in value.values[0].saverecord: - # print("SaveRecord") + if hasattr(value.values[0], "printrecord") and isinstance( + value.values[0].printrecord, list + ): + action = "PRINT" + for rec in value.values[0].printrecord: + key = f"{action} {rec.rtype}" + data[key] = oc_setting_data(rec, action) + if hasattr(value.values[0], "saverecord") and isinstance( + value.values[0].saverecord, list + ): + action = "SAVE" + for rec in value.values[0].saverecord: # type: ignore + key = f"{action} {rec.rtype}" + data[key] = oc_setting_data(rec, action) return data @@ -209,7 +236,6 @@ def unstructure_component(value: Component) -> dict[str, Any]: # sort kper order period_blocks = dict(sorted(period_blocks.items())) - print(period_blocks) # setup indexed period blocks, combine arrays into datasets for kper, block in period_blocks.items(): diff --git a/test/test_mf6_codec.py b/test/test_mf6_codec.py index 843ba5ed..0675af28 100644 --- a/test/test_mf6_codec.py +++ b/test/test_mf6_codec.py @@ -70,6 +70,9 @@ def test_dumps_sto(): dumped = dumps(COMPONENT_CONVERTER.unstructure(sto)) print("STO dump:") print(dumped) + assert "BEGIN PERIOD 1\n TRANSIENT" in dumped + assert "BEGIN PERIOD 2\n STEADY_STATE" in dumped + assert "BEGIN PERIOD 3\n TRANSIENT" in dumped assert dumped loaded = loads(dumped) @@ -110,6 +113,44 @@ def test_dumps_oc(): pprint(loaded) +def test_dumps_oc2(): + from flopy4.mf6.gwf import Oc + + oc = Oc( + dims={"nper": 1}, + budget_file="test.bud", + head_file="test.hds", + # save_head={0: "all"}, + # save_budget={0: "all"}, + perioddata={ + 0: Oc.PrintSaveSetting( + printrecord=[ + Oc.PrintRecord("head", Oc.Steps(first=True)), + # Oc.PrintRecord("budget", Oc.Steps(last=True)), + Oc.PrintRecord("budget", Oc.Steps(steps=(2, 3, 5))), + ], + saverecord=[ + Oc.SaveRecord("head", Oc.Steps(last=True)), + Oc.SaveRecord("budget", Oc.Steps(first=True)), + ], + ) + }, + ) + + dumped = dumps(COMPONENT_CONVERTER.unstructure(oc)) + print("OC dump:") + print(dumped) + assert "SAVE HEAD last" in dumped + assert "SAVE BUDGET first" in dumped + assert "PRINT HEAD first" in dumped + assert "PRINT BUDGET steps 1 2 4" in dumped + assert dumped + + loaded = loads(dumped) + print("OC load:") + pprint(loaded) + + def test_dumps_dis(): from flopy4.mf6.gwf import Dis From 29d9f14d06148fc1be71565d1f8acbcde6cf8647 Mon Sep 17 00:00:00 2001 From: mjreno Date: Wed, 5 Nov 2025 09:51:40 -0500 Subject: [PATCH 6/7] cleanup --- flopy4/mf6/converter/unstructure.py | 6 ++---- test/test_mf6_codec.py | 3 --- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index ead67114..0b30445f 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -95,24 +95,22 @@ def _hack_period_non_numeric(name: str, value: xr.DataArray) -> dict[str, dict[i def oc_setting_data(rec, action): dat = {} + key = f"{action} {rec.rtype}" if rec.steps.first: dat = {kper: "first" for kper in range(value.sizes["nper"])} - key = f"{action} {rec.rtype}" data[key] = dat elif rec.steps.last: dat = {kper: "last" for kper in range(value.sizes["nper"])} - key = f"{action} {rec.rtype}" data[key] = dat elif rec.steps.steps: steps = " ".join(str(x - 1) for x in rec.steps.steps) dat = {kper: f"steps {steps}" for kper in range(value.sizes["nper"])} - key = f"{action} {rec.rtype}" data[key] = dat elif rec.steps.all: # check last as this defaults to True dat = {kper: "all" for kper in range(value.sizes["nper"])} - key = f"{action} {rec.rtype}" data[key] = dat + return dat data = {} diff --git a/test/test_mf6_codec.py b/test/test_mf6_codec.py index 0675af28..1e1e623a 100644 --- a/test/test_mf6_codec.py +++ b/test/test_mf6_codec.py @@ -120,13 +120,10 @@ def test_dumps_oc2(): dims={"nper": 1}, budget_file="test.bud", head_file="test.hds", - # save_head={0: "all"}, - # save_budget={0: "all"}, perioddata={ 0: Oc.PrintSaveSetting( printrecord=[ Oc.PrintRecord("head", Oc.Steps(first=True)), - # Oc.PrintRecord("budget", Oc.Steps(last=True)), Oc.PrintRecord("budget", Oc.Steps(steps=(2, 3, 5))), ], saverecord=[ From f726a5588aec7038ebe6936446ae8e7a79cc374e Mon Sep 17 00:00:00 2001 From: mjreno Date: Wed, 5 Nov 2025 10:02:48 -0500 Subject: [PATCH 7/7] more cleanup --- flopy4/mf6/converter/unstructure.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/flopy4/mf6/converter/unstructure.py b/flopy4/mf6/converter/unstructure.py index 0b30445f..74faa582 100644 --- a/flopy4/mf6/converter/unstructure.py +++ b/flopy4/mf6/converter/unstructure.py @@ -93,23 +93,18 @@ def _hack_structured_grid_dims( def _hack_period_non_numeric(name: str, value: xr.DataArray) -> dict[str, dict[int, Any]]: from flopy4.mf6.gwf import Oc - def oc_setting_data(rec, action): + def oc_setting_data(rec): dat = {} - key = f"{action} {rec.rtype}" if rec.steps.first: dat = {kper: "first" for kper in range(value.sizes["nper"])} - data[key] = dat elif rec.steps.last: dat = {kper: "last" for kper in range(value.sizes["nper"])} - data[key] = dat elif rec.steps.steps: steps = " ".join(str(x - 1) for x in rec.steps.steps) dat = {kper: f"steps {steps}" for kper in range(value.sizes["nper"])} - data[key] = dat elif rec.steps.all: # check last as this defaults to True dat = {kper: "all" for kper in range(value.sizes["nper"])} - data[key] = dat return dat @@ -127,17 +122,15 @@ def oc_setting_data(rec, action): if hasattr(value.values[0], "printrecord") and isinstance( value.values[0].printrecord, list ): - action = "PRINT" for rec in value.values[0].printrecord: - key = f"{action} {rec.rtype}" - data[key] = oc_setting_data(rec, action) + key = f"{rec.print} {rec.rtype}" + data[key] = oc_setting_data(rec) if hasattr(value.values[0], "saverecord") and isinstance( value.values[0].saverecord, list ): - action = "SAVE" for rec in value.values[0].saverecord: # type: ignore - key = f"{action} {rec.rtype}" - data[key] = oc_setting_data(rec, action) + key = f"{rec.save} {rec.rtype}" # type: ignore + data[key] = oc_setting_data(rec) return data