From 11cfd6de443c430b63aa4afa74cc0d72c10bfd99 Mon Sep 17 00:00:00 2001 From: mjreno Date: Thu, 9 Oct 2025 08:40:32 -0400 Subject: [PATCH 1/7] baseline fixing existing tests --- .../mf6/codec/writer/templates/macros.jinja | 6 ++- flopy4/mf6/converter.py | 10 ++-- flopy4/mf6/solution.py | 3 ++ test/test_component.py | 50 +++++++++++++++++-- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/flopy4/mf6/codec/writer/templates/macros.jinja b/flopy4/mf6/codec/writer/templates/macros.jinja index e991219a..57fb1502 100644 --- a/flopy4/mf6/codec/writer/templates/macros.jinja +++ b/flopy4/mf6/codec/writer/templates/macros.jinja @@ -1,3 +1,5 @@ +{% set inset = " " %} + {% macro field(name, value) %} {% set format = value|field_format %} {% if format in ['keyword', 'integer', 'double precision', 'string'] %} @@ -15,7 +17,7 @@ {% macro scalar(name, value) %} {% set format = value|field_format %} -{% if value is not none %}{{ name.upper() }}{% if format != 'keyword' %} {{ value }}{% endif %}{% endif %} +{% if value is not none %}{{ inset ~ name.upper() }}{% if format != 'keyword' %} {{ value }}{% endif %}{% endif %} {% endmacro %} {% macro keystring(name, value) %} @@ -55,6 +57,6 @@ OPEN/CLOSE {{ value }} {% macro list(name, value) %} {% for row in (value|data2list) %} -{{ row|join(" ") }} +{{ inset ~ row|join(" ") }} {% endfor %} {% endmacro %} diff --git a/flopy4/mf6/converter.py b/flopy4/mf6/converter.py index 0ae1fc9f..18bde58b 100644 --- a/flopy4/mf6/converter.py +++ b/flopy4/mf6/converter.py @@ -47,6 +47,8 @@ def _get_binding_type(component: Component) -> str: cls_name = component.__class__.__name__ if isinstance(component, Exchange): return f"{'-'.join([cls_name[:2], cls_name[3:]]).upper()}6" + elif isinstance(component, Solution): + return "IMS6" else: return f"{cls_name.upper()}6" @@ -107,7 +109,7 @@ def unstructure_component(value: Component) -> dict[str, Any]: for comp in field_value.values() if comp is not None ] - elif isinstance(field_value, (list, tuple)): + elif isinstance(field_value, (list, tuple, xattree.DataTreeList)): components = [ _Binding.from_component(comp).to_tuple() for comp in field_value @@ -170,16 +172,16 @@ def unstructure_component(value: Component) -> dict[str, Any]: ( field_value.sizes["nper"], parent.dims["nlay"], - parent.dims["ncol"], parent.dims["nrow"], + parent.dims["ncol"], ) ), - dims=("nper", "nlay", "ncol", "nrow"), + dims=("nper", "nlay", "nrow", "ncol"), coords={ "nper": field_value.coords["nper"], "nlay": range(parent.dims["nlay"]), - "ncol": range(parent.dims["ncol"]), "nrow": range(parent.dims["nrow"]), + "ncol": range(parent.dims["ncol"]), }, name=field_value.name, ) diff --git a/flopy4/mf6/solution.py b/flopy4/mf6/solution.py index d3b63b17..22a2b57d 100644 --- a/flopy4/mf6/solution.py +++ b/flopy4/mf6/solution.py @@ -9,3 +9,6 @@ @xattree class Solution(Package, ABC): models: list[str] = attrs.field(default=attrs.Factory(list)) + + def default_filename(self) -> str: + return f"{self.name}.ims" # type: ignore diff --git a/test/test_component.py b/test/test_component.py index 46a17162..2ec554ff 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -12,6 +12,7 @@ from flopy4.mf6.gwf import Chd, Dis, Gwf, Ic, Npf, Oc from flopy4.mf6.ims import Ims from flopy4.mf6.simulation import Simulation +from flopy4.mf6.solution import Solution from flopy4.mf6.tdis import Tdis @@ -274,13 +275,56 @@ def test_ims_dfn(): assert "inner_maximum" in set(dfn["linear"].keys()) +def test_chd02(function_tmpdir): + sim_name = "chd02" + time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) + sln = Solution(models=["gwf"]) + dis = Dis( + nlay=1, + nrow=1, + ncol=10, + delr=1.0, + delc=1.0, + top=10.0, + botm=0.0, + ) + sim = Simulation( + tdis=time, + workspace=function_tmpdir, + name=sim_name, + solutions={"ims": sln}, + ) + gwf_name = "gwf" + gwf = Gwf(parent=sim, dis=dis, name=gwf_name) + ic = Ic(parent=gwf, strt=10.0) + oc = Oc(parent=gwf) + npf = Npf(parent=gwf, icelltype=1) + chd = Chd(parent=gwf, head={0: {(0, 0, 0): 10.0, (0, 0, 9): 5.0}}) + + sim.write() + + def test_write_ascii(function_tmpdir): sim_name = "sim" time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) - grid = StructuredGrid(nlay=1, nrow=10, ncol=10) - sim = Simulation(tdis=time, workspace=function_tmpdir, name=sim_name) + sln = Solution(models=["gwf"]) + dis = Dis( + nlay=1, + nrow=10, + ncol=10, + delr=1.0, + delc=1.0, + top=1.0, + botm=0.0, + ) + sim = Simulation( + tdis=time, + workspace=function_tmpdir, + name=sim_name, + solutions={"ims": sln}, + ) gwf_name = "gwf" - gwf = Gwf(parent=sim, dis=grid, name=gwf_name) + gwf = Gwf(parent=sim, dis=dis, name=gwf_name) ic = Ic(parent=gwf) oc = Oc(parent=gwf) npf = Npf(parent=gwf) From 29be0da49f60a9f68ab80bef7efd5578071ad7a0 Mon Sep 17 00:00:00 2001 From: mjreno Date: Thu, 9 Oct 2025 17:13:42 -0400 Subject: [PATCH 2/7] options kw fix, enhance sln --- flopy4/mf6/converter.py | 8 +++-- flopy4/mf6/ims.py | 6 ++-- flopy4/mf6/solution.py | 11 +++++-- test/test_component.py | 71 ++++++++++++++++++++++++++++++----------- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/flopy4/mf6/converter.py b/flopy4/mf6/converter.py index 18bde58b..2b2e426d 100644 --- a/flopy4/mf6/converter.py +++ b/flopy4/mf6/converter.py @@ -48,7 +48,7 @@ def _get_binding_type(component: Component) -> str: if isinstance(component, Exchange): return f"{'-'.join([cls_name[:2], cls_name[3:]]).upper()}6" elif isinstance(component, Solution): - return "IMS6" + return f"{component.slntype}6" else: return f"{cls_name.upper()}6" @@ -196,7 +196,11 @@ def unstructure_component(value: Component) -> dict[str, Any]: period_data[block_name][field_name] = field_value # type: ignore else: if field_value is not None: - blocks[block_name][field_name] = field_value + if isinstance(field_value, bool): + if field_value: + blocks[block_name][field_name] = field_value + else: + blocks[block_name][field_name] = field_value if block_name in period_data and isinstance(period_data[block_name], dict): dataset = xr.Dataset(period_data[block_name]) diff --git a/flopy4/mf6/ims.py b/flopy4/mf6/ims.py index 98392f19..9e2a0e70 100644 --- a/flopy4/mf6/ims.py +++ b/flopy4/mf6/ims.py @@ -19,9 +19,9 @@ class Ims(Solution): no_ptc: bool = field(default=False, block="options") no_ptc_option: Optional[str] = field(default=None, block="options") ats_outer_maximum_fraction: Optional[float] = field(block="options", default=None) - outer_dvclose: Optional[float] = field(default=None, block="options") - outer_maximum: Optional[int] = field(default=None, block="options") - under_relaxation: Optional[str] = field(default=None, block="options") + outer_dvclose: Optional[float] = field(default=None, block="nonlinear") + outer_maximum: Optional[int] = field(default=None, block="nonlinear") + under_relaxation: Optional[str] = field(default=None, block="nonlinear") under_relaxation_gamma: Optional[float] = field(block="nonlinear", default=None) under_relaxation_theta: Optional[float] = field(block="nonlinear", default=None) under_relaxation_kappa: Optional[float] = field(block="nonlinear", default=None) diff --git a/flopy4/mf6/solution.py b/flopy4/mf6/solution.py index 22a2b57d..6fc88c86 100644 --- a/flopy4/mf6/solution.py +++ b/flopy4/mf6/solution.py @@ -1,14 +1,21 @@ from abc import ABC +from pathlib import Path +from typing import Optional import attrs -from xattree import xattree +from xattree import field, xattree from flopy4.mf6.package import Package @xattree class Solution(Package, ABC): + slntype: Optional[str] = field(default=None) # type: ignore + slnfname: Optional[Path] = field(default=None) # type: ignore models: list[str] = attrs.field(default=attrs.Factory(list)) + mxiter: int = field(default=1) def default_filename(self) -> str: - return f"{self.name}.ims" # type: ignore + name = self.slntype.lower() if self.slntype else "sln" + cls_name = self.__class__.__name__.lower() + return self.slnfname if self.slnfname else f"{cls_name}.{name}" diff --git a/test/test_component.py b/test/test_component.py index 2ec554ff..e449a831 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -264,7 +264,7 @@ def test_chd_dfn(): def test_ims_dfn(): - ims = Ims(strict=False) + ims = Ims(slntype="ims", strict=False) dfn = ims.dfn assert dfn["name"] == "ims" assert not dfn["advanced"] @@ -275,31 +275,66 @@ def test_ims_dfn(): assert "inner_maximum" in set(dfn["linear"].keys()) -def test_chd02(function_tmpdir): - sim_name = "chd02" - time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) - sln = Solution(models=["gwf"]) +def test_gwf_chd01(function_tmpdir): + sim_name = "chd01" + gwf_name = "gwf_chd01" + time = ModelTime(perlen=[5.0], nstp=[1], tsmult=[1.0], time_units="days") + + ims = Ims( + slntype="ims", + slnfname="sln1.ims", + models=[gwf_name], + print_option="summary", + outer_dvclose=1.00000000e-06, + outer_maximum=100, + under_relaxation="none", + inner_maximum=300, + inner_dvclose=1.00000000e-06, + inner_rclose=1.00000000e-06, + linear_acceleration="cg", + relaxation_factor=1.0, + scaling_method="none", + reordering_method="none", + ) + + sim = Simulation( + tdis=time, + workspace=function_tmpdir, + name=sim_name, + solutions={"ims": ims}, + ) + dis = Dis( nlay=1, nrow=1, - ncol=10, + ncol=100, delr=1.0, delc=1.0, - top=10.0, + top=1.0, botm=0.0, + idomain=1, ) - sim = Simulation( - tdis=time, - workspace=function_tmpdir, - name=sim_name, - solutions={"ims": sln}, - ) - gwf_name = "gwf" - gwf = Gwf(parent=sim, dis=dis, name=gwf_name) - ic = Ic(parent=gwf, strt=10.0) + + gwf = Gwf(parent=sim, save_flows=True, dis=dis, name=gwf_name) + + ic = Ic(parent=gwf, strt=1.0) + oc = Oc(parent=gwf) - npf = Npf(parent=gwf, icelltype=1) - chd = Chd(parent=gwf, head={0: {(0, 0, 0): 10.0, (0, 0, 9): 5.0}}) + + npf = Npf( + parent=gwf, + save_specific_discharge=True, + k=1.0, + k33=1.0, + icelltype=0, + ) + + chd = Chd( + parent=gwf, + print_flows=True, + head={0: {(0, 0, 0): 1.0, (0, 0, 99): 0.0}}, + name="chd-1", + ) sim.write() From 63ece2622b00b19fafdef2fbd58d4f2220523a34 Mon Sep 17 00:00:00 2001 From: mjreno Date: Thu, 9 Oct 2025 17:17:42 -0400 Subject: [PATCH 3/7] lint path as str --- flopy4/mf6/solution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flopy4/mf6/solution.py b/flopy4/mf6/solution.py index 6fc88c86..fffd0375 100644 --- a/flopy4/mf6/solution.py +++ b/flopy4/mf6/solution.py @@ -18,4 +18,4 @@ class Solution(Package, ABC): def default_filename(self) -> str: name = self.slntype.lower() if self.slntype else "sln" cls_name = self.__class__.__name__.lower() - return self.slnfname if self.slnfname else f"{cls_name}.{name}" + return str(self.slnfname) if self.slnfname else f"{cls_name}.{name}" From 3794d0adb4e0c50c8eb2347b697f98abe8fc4c73 Mon Sep 17 00:00:00 2001 From: mjreno Date: Thu, 9 Oct 2025 22:44:51 -0400 Subject: [PATCH 4/7] move slntype to derived --- flopy4/mf6/ims.py | 2 ++ flopy4/mf6/solution.py | 7 ++----- test/test_component.py | 10 ++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/flopy4/mf6/ims.py b/flopy4/mf6/ims.py index 9e2a0e70..e46fb65a 100644 --- a/flopy4/mf6/ims.py +++ b/flopy4/mf6/ims.py @@ -11,7 +11,9 @@ @xattree class Ims(Solution): solution_package: ClassVar[Sln] = Sln(abbr="ims", pattern="*") + slntype: ClassVar[str] = "ims" + mxiter: Optional[int] = field(default=1) print_option: Optional[str] = field(block="options", default=None) complexity: str = field(block="options", default="simple") csv_outer_output_file: Optional[Path] = field(default=None, block="options") diff --git a/flopy4/mf6/solution.py b/flopy4/mf6/solution.py index fffd0375..d9154a5b 100644 --- a/flopy4/mf6/solution.py +++ b/flopy4/mf6/solution.py @@ -10,12 +10,9 @@ @xattree class Solution(Package, ABC): - slntype: Optional[str] = field(default=None) # type: ignore slnfname: Optional[Path] = field(default=None) # type: ignore models: list[str] = attrs.field(default=attrs.Factory(list)) - mxiter: int = field(default=1) def default_filename(self) -> str: - name = self.slntype.lower() if self.slntype else "sln" - cls_name = self.__class__.__name__.lower() - return str(self.slnfname) if self.slnfname else f"{cls_name}.{name}" + ftype = self.slntype.lower() if self.slntype else "sln" + return str(self.slnfname) if self.slnfname else f"solution.{ftype}" diff --git a/test/test_component.py b/test/test_component.py index e449a831..5ffc474f 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -12,7 +12,6 @@ from flopy4.mf6.gwf import Chd, Dis, Gwf, Ic, Npf, Oc from flopy4.mf6.ims import Ims from flopy4.mf6.simulation import Simulation -from flopy4.mf6.solution import Solution from flopy4.mf6.tdis import Tdis @@ -264,7 +263,7 @@ def test_chd_dfn(): def test_ims_dfn(): - ims = Ims(slntype="ims", strict=False) + ims = Ims(strict=False) dfn = ims.dfn assert dfn["name"] == "ims" assert not dfn["advanced"] @@ -281,7 +280,6 @@ def test_gwf_chd01(function_tmpdir): time = ModelTime(perlen=[5.0], nstp=[1], tsmult=[1.0], time_units="days") ims = Ims( - slntype="ims", slnfname="sln1.ims", models=[gwf_name], print_option="summary", @@ -341,8 +339,9 @@ def test_gwf_chd01(function_tmpdir): def test_write_ascii(function_tmpdir): sim_name = "sim" + gwf_name = "gwf" time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) - sln = Solution(models=["gwf"]) + ims = Ims(models=[gwf_name]) dis = Dis( nlay=1, nrow=10, @@ -356,9 +355,8 @@ def test_write_ascii(function_tmpdir): tdis=time, workspace=function_tmpdir, name=sim_name, - solutions={"ims": sln}, + solutions={"ims": ims}, ) - gwf_name = "gwf" gwf = Gwf(parent=sim, dis=dis, name=gwf_name) ic = Ic(parent=gwf) oc = Oc(parent=gwf) From 0e5f15cf1f00e9f8e2dd1392fb30f807057da059 Mon Sep 17 00:00:00 2001 From: mjreno Date: Thu, 9 Oct 2025 22:51:56 -0400 Subject: [PATCH 5/7] fix slntype --- flopy4/mf6/solution.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flopy4/mf6/solution.py b/flopy4/mf6/solution.py index d9154a5b..ba444e24 100644 --- a/flopy4/mf6/solution.py +++ b/flopy4/mf6/solution.py @@ -1,6 +1,6 @@ from abc import ABC from pathlib import Path -from typing import Optional +from typing import ClassVar, Optional import attrs from xattree import field, xattree @@ -10,9 +10,10 @@ @xattree class Solution(Package, ABC): + slntype: ClassVar[str] = "sln" + slnfname: Optional[Path] = field(default=None) # type: ignore models: list[str] = attrs.field(default=attrs.Factory(list)) def default_filename(self) -> str: - ftype = self.slntype.lower() if self.slntype else "sln" - return str(self.slnfname) if self.slnfname else f"solution.{ftype}" + return str(self.slnfname) if self.slnfname else f"solution.{self.slntype.lower()}" From e9e4377dd514c114806aaa9fd9397fbedf8624d6 Mon Sep 17 00:00:00 2001 From: mjreno Date: Fri, 10 Oct 2025 14:44:36 -0400 Subject: [PATCH 6/7] take a stab at oc --- flopy4/mf6/codec/writer/filters.py | 7 +++++- .../mf6/codec/writer/templates/macros.jinja | 2 +- flopy4/mf6/converter.py | 22 ++++++++++++++----- test/test_component.py | 11 +++++++++- test/test_interface.py | 3 --- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/flopy4/mf6/codec/writer/filters.py b/flopy4/mf6/codec/writer/filters.py index 573117b9..bf64ba55 100644 --- a/flopy4/mf6/codec/writer/filters.py +++ b/flopy4/mf6/codec/writer/filters.py @@ -260,9 +260,14 @@ def data2keystring(value: dict | xr.Dataset): return for field_name in value.data_vars.keys(): + name = ( + field_name.replace("_", " ").upper() + if np.issubdtype(value.data_vars[field_name].dtype, np.str_) + else field_name.upper() + ) field_val = value[field_name] if hasattr(field_val, "item"): val = field_val.item() else: val = field_val - yield (field_name.upper(), val) + yield (name, val) diff --git a/flopy4/mf6/codec/writer/templates/macros.jinja b/flopy4/mf6/codec/writer/templates/macros.jinja index 57fb1502..0feb00b4 100644 --- a/flopy4/mf6/codec/writer/templates/macros.jinja +++ b/flopy4/mf6/codec/writer/templates/macros.jinja @@ -32,7 +32,7 @@ {{ field_name.upper() }} {{ field(field_value) }} {%- endfor %} {% else %} -{{ value|join(" ") }} +{{ inset ~ value|join(" ") }} {%- endif %} {% endmacro %} diff --git a/flopy4/mf6/converter.py b/flopy4/mf6/converter.py index 2b2e426d..16db8e35 100644 --- a/flopy4/mf6/converter.py +++ b/flopy4/mf6/converter.py @@ -1,4 +1,4 @@ -from collections.abc import MutableMapping +from collections.abc import Iterable, MutableMapping from datetime import datetime from pathlib import Path from typing import Any @@ -109,7 +109,7 @@ def unstructure_component(value: Component) -> dict[str, Any]: for comp in field_value.values() if comp is not None ] - elif isinstance(field_value, (list, tuple, xattree.DataTreeList)): + elif isinstance(field_value, Iterable): components = [ _Binding.from_component(comp).to_tuple() for comp in field_value @@ -191,9 +191,21 @@ def unstructure_component(value: Component) -> dict[str, Any]: for kper in range(field_value.sizes["nper"]) } else: - if block_name not in period_data: - period_data[block_name] = {} - period_data[block_name][field_name] = field_value # type: ignore + if ( + # TODO: refactor + # field_name == "save_budget" + # or field_name == "save_head" + # or field_name == "print_budget" + # or field_name == "print_head" + np.issubdtype(field_value.dtype, np.str_) + ): + period_data[field_name] = { + kper: field_value[kper] for kper in range(field_value.sizes["nper"]) + } + else: + if block_name not in period_data: + period_data[block_name] = {} + period_data[block_name][field_name] = field_value # type: ignore else: if field_value is not None: if isinstance(field_value, bool): diff --git a/test/test_component.py b/test/test_component.py index 5ffc474f..94111002 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -317,7 +317,16 @@ def test_gwf_chd01(function_tmpdir): ic = Ic(parent=gwf, strt=1.0) - oc = Oc(parent=gwf) + oc = Oc( + parent=gwf, + budget_file=f"{gwf_name}.cbc", + head_file=f"{gwf_name}.hds", + # COLUMNS 10 WIDTH 15 DIGITS 6 GENERAL + save_head=["last"], + save_budget=["last"], + print_head=["last"], + print_budget=["last"], + ) npf = Npf( parent=gwf, diff --git a/test/test_interface.py b/test/test_interface.py index 66d0a889..75d4e697 100644 --- a/test/test_interface.py +++ b/test/test_interface.py @@ -242,9 +242,6 @@ def test_flopy3_package(tmp_path): def norun_test_flopy3_cbd_small(tmp_path): - import sys - - sys.path.append("/home/mjreno/.clone/usgs/flopy/autotest") from test_grid_cases import GridCases time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) From 92a27a80ead29c8fa145f1b5c0e37dea9a3e6ea3 Mon Sep 17 00:00:00 2001 From: mjreno Date: Fri, 10 Oct 2025 16:07:51 -0400 Subject: [PATCH 7/7] add quickstart test --- test/test_component.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/test_component.py b/test/test_component.py index 94111002..0aab3f1a 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -323,6 +323,7 @@ def test_gwf_chd01(function_tmpdir): head_file=f"{gwf_name}.hds", # COLUMNS 10 WIDTH 15 DIGITS 6 GENERAL save_head=["last"], + # save_head={0: "last"}, save_budget=["last"], print_head=["last"], print_budget=["last"], @@ -344,6 +345,41 @@ def test_gwf_chd01(function_tmpdir): ) sim.write() + sim.run() + + +def test_quickstart(function_tmpdir): + sim_name = "quickstart" + gwf_name = "mymodel" + time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0]) + ims = Ims(models=[gwf_name]) + dis = Dis( + nlay=1, + nrow=10, + ncol=10, + top=1.0, + botm=0.0, + ) + sim = Simulation( + tdis=time, + workspace=function_tmpdir, + name=sim_name, + solutions={"ims": ims}, + ) + gwf = Gwf(parent=sim, dis=dis, name=gwf_name) + ic = Ic(parent=gwf) + oc = Oc( + parent=gwf, + budget_file=f"{gwf_name}.bud", + head_file=f"{gwf_name}.hds", + save_head=["all"], + save_budget=["all"], + ) + npf = Npf(parent=gwf, icelltype=0, k=1.0) + chd = Chd(parent=gwf, head={0: {(0, 0, 0): 1.0, (0, 9, 9): 0.0}}) + + sim.write() + sim.run() def test_write_ascii(function_tmpdir):