Skip to content

Commit eb141a0

Browse files
committed
component filename, default fmt ascii, revert sim path -> workspace
1 parent 80c9912 commit eb141a0

File tree

9 files changed

+204
-172
lines changed

9 files changed

+204
-172
lines changed

docs/examples/quickstart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
workspace = Path(__file__).parent / name
1414
time = ModelTime(perlen=[1.0], nstp=[1])
1515
grid = StructuredGrid(nlay=1, nrow=10, ncol=10)
16-
sim = Simulation(name=name, path=workspace, tdis=time)
16+
sim = Simulation(name=name, workspace=workspace, tdis=time)
1717
ims = Ims(parent=sim)
1818
gwf_name = "mymodel"
1919
gwf = Gwf(parent=sim, name=gwf_name, save_flows=True, dis=grid)

docs/examples/quickstart_expanded.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
ws = "./mymodel"
3333
name = "mymodel"
34-
sim = Simulation(name=name, path=ws, exe="mf6")
34+
sim = Simulation(name=name, workspace=ws, exe="mf6")
3535
tdis = Tdis(sim)
3636
gwf = Gwf(sim, name=name, save_flows=True)
3737
dis = Dis(gwf, nrow=10, ncol=10)

flopy4/mf6/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
def _default_filename(component: Component) -> str:
99
"""Default path for a component, based on its name."""
10-
if hasattr(component, "filename") and component.filename is not None:
11-
return component.filename
10+
if filename := component.filename:
11+
return filename
1212
name = component.name # type: ignore
1313
cls_name = component.__class__.__name__.lower()
1414
return f"{name}.{cls_name}"

flopy4/mf6/component.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
from modflow_devtools.dfn import Dfn, Field
55
from xattree import xattree
66

7-
from flopy4.mf6.spec import fields_dict, to_dfn_field
7+
from flopy4.mf6.spec import field, fields_dict, to_dfn_field
88
from flopy4.uio import IO, Loader, Writer
99

1010
COMPONENTS = {}
1111
"""MF6 component registry."""
1212

1313

14-
@xattree
14+
# kw_only=True necessary so we can define optional fields here
15+
# and required fields in subclasses. attrs complains otherwise
16+
@xattree(kw_only=True)
1517
class Component(ABC, MutableMapping):
1618
"""
1719
Base class for MF6 components.
@@ -22,11 +24,14 @@ class Component(ABC, MutableMapping):
2224
2325
We use the `children` attribute provided by `xattree`. We know
2426
children are also `Component`s, but mypy does not. TODO: fix??
27+
Then we can remove the `# type: ignore` comments.
2528
"""
2629

2730
_load = IO(Loader) # type: ignore
2831
_write = IO(Writer) # type: ignore
2932

33+
filename: str = field(default=None)
34+
3035
@classmethod
3136
def __attrs_init_subclass__(cls):
3237
COMPONENTS[cls.__name__.lower()] = cls
@@ -49,13 +54,14 @@ def __len__(self):
4954

5055
@classmethod
5156
def get_dfn(cls) -> Dfn:
57+
"""Generate the component's MODFLOW 6 definition."""
5258
fields = {field_name: to_dfn_field(field) for field_name, field in fields_dict(cls).items()}
5359
blocks: dict[str, dict[str, Field]] = {}
54-
for field_name, field in fields.items():
55-
if (block := field.get("block", None)) is not None:
56-
blocks.setdefault(block, {})[field_name] = field
60+
for field_name, field_ in fields.items():
61+
if (block := field_.get("block", None)) is not None:
62+
blocks.setdefault(block, {})[field_name] = field_
5763
else:
58-
blocks[field_name] = field
64+
blocks[field_name] = field_
5965

6066
return Dfn(
6167
name=cls.__name__.lower(),

flopy4/mf6/exchange.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from abc import ABC
12
from pathlib import Path
23
from typing import Optional
34

@@ -7,8 +8,10 @@
78

89

910
@xattree
10-
class Exchange(Package):
11-
exgtype: type = field()
12-
exgfile: Path = field()
11+
class Exchange(Package, ABC):
12+
# mypy doesn't understand that kw_only=True on the
13+
# Component means we can have required fields here
14+
exgtype: type = field() # type: ignore
15+
exgfile: Path = field() # type: ignore
1316
exgmnamea: Optional[str] = field(default=None)
1417
exgmnameb: Optional[str] = field(default=None)

flopy4/mf6/simulation.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from os import PathLike
22
from pathlib import Path
3-
from typing import ClassVar
3+
from warnings import warn
44

55
from flopy.discretization.modeltime import ModelTime
66
from modflow_devtools.misc import cd, run_cmd
7-
from xattree import field, xattree
7+
from xattree import xattree
88

99
from flopy4.mf6.component import Component
1010
from flopy4.mf6.exchange import Exchange
1111
from flopy4.mf6.model import Model
1212
from flopy4.mf6.solution import Solution
13+
from flopy4.mf6.spec import field
1314
from flopy4.mf6.tdis import Tdis
1415

1516

@@ -27,31 +28,49 @@ class Simulation(Component):
2728
exchanges: dict[str, Exchange] = field()
2829
solutions: dict[str, Solution] = field()
2930
tdis: Tdis = field(converter=convert_time)
30-
# TODO: decorator for components bound
31-
# to some directory or file path?
32-
path: Path = field(default=None)
33-
filename: ClassVar[str] = "mfsim.nam"
31+
workspace: Path = field(default=None)
32+
filename: str = field(default="mfsim.nam")
33+
34+
def __attrs_post_init__(self):
35+
"""Post-initialization hook to set up the simulation."""
36+
super().__attrs_post_init__()
37+
if self.filename != "mfsim.nam":
38+
warn(
39+
"Simulation filename must be 'mfsim.nam'.",
40+
UserWarning,
41+
)
42+
self.filename = "mfsim.nam"
43+
44+
@property
45+
def path(self) -> Path:
46+
"""Return the path to the simulation namefile."""
47+
if self.workspace is None:
48+
raise ValueError("Simulation has no workspace path.")
49+
return Path(self.workspace).expanduser().resolve() / self.filename
3450

3551
@property
3652
def time(self) -> ModelTime:
53+
"""Return the simulation time discretization."""
3754
return self.tdis.to_time()
3855

3956
def run(self, exe: str | PathLike = "mf6", verbose: bool = False) -> None:
4057
"""Run the simulation using the given executable."""
41-
if self.path is None:
58+
if self.workspace is None:
4259
raise ValueError(f"Simulation {self.name} has no workspace path.")
43-
with cd(self.path):
60+
with cd(self.workspace):
4461
stdout, stderr, retcode = run_cmd(exe, verbose=verbose)
4562
if retcode != 0:
4663
raise RuntimeError(
4764
f"Simulation {self.name}: {exe} failed to run with returncode " # type: ignore
4865
f"{retcode}, and error message:\n\n{stdout + stderr} "
4966
)
5067

51-
def load(self, format):
52-
with cd(self.path):
68+
def load(self, format="ascii"):
69+
"""Load the simulation in the specified format."""
70+
with cd(self.workspace):
5371
super().load(format)
5472

55-
def write(self, format):
56-
with cd(self.path):
73+
def write(self, format="ascii"):
74+
"""Write the simulation in the specified format."""
75+
with cd(self.workspace):
5776
super().write(format)

0 commit comments

Comments
 (0)