Skip to content

Commit 282e6bd

Browse files
authored
component filename, default fmt ascii, revert sim path -> workspace (#146)
* move filename field to Component. this required supporting kw_only on xattree classes (passed thru to attrs.define) which lets us put required/optional fields in any order. otherwise attrs dictates required fields must come first therefore an optional filename on Component would mean no required fields on concrete component types. * set the default load/write format to ascii on Simulation * rename Simulation.path back to workspace as it was originally.. introduce an intermediate component subclass Context which is a component with a workspace * make path a derived property on Component and Context
1 parent 80c9912 commit 282e6bd

File tree

11 files changed

+234
-202
lines changed

11 files changed

+234
-202
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: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,8 @@
1-
from pathlib import Path
2-
31
from flopy4.mf6.codec import dump, load
42
from flopy4.mf6.component import Component
53
from flopy4.uio import DEFAULT_REGISTRY
64

7-
8-
def _default_filename(component: Component) -> str:
9-
"""Default path for a component, based on its name."""
10-
if hasattr(component, "filename") and component.filename is not None:
11-
return component.filename
12-
name = component.name # type: ignore
13-
cls_name = component.__class__.__name__.lower()
14-
return f"{name}.{cls_name}"
15-
16-
17-
def _path(component: Component) -> str:
18-
"""Default path for a component, based on its name."""
19-
if hasattr(component, "path") and component.path is not None:
20-
path = Path(component.path).expanduser().resolve()
21-
if path.is_dir():
22-
return str(path / _default_filename(component))
23-
return str(path)
24-
return _default_filename(component)
25-
26-
27-
DEFAULT_REGISTRY.register_loader(Component, "ascii", lambda component: load(_path(component)))
28-
DEFAULT_REGISTRY.register_writer(
29-
Component, "ascii", lambda component: dump(component, _path(component))
30-
)
5+
# register io methods
6+
# TODO: call this "mf6" or something? since it might include binary files
7+
DEFAULT_REGISTRY.register_loader(Component, "ascii", lambda c: load(c.path))
8+
DEFAULT_REGISTRY.register_writer(Component, "ascii", lambda c: dump(c, c.path))

flopy4/mf6/component.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from abc import ABC
22
from collections.abc import MutableMapping
3+
from pathlib import Path
34

45
from modflow_devtools.dfn import Dfn, Field
56
from xattree import xattree
67

7-
from flopy4.mf6.spec import fields_dict, to_dfn_field
8+
from flopy4.mf6.spec import field, fields_dict, to_dfn_field
89
from flopy4.uio import IO, Loader, Writer
910

1011
COMPONENTS = {}
1112
"""MF6 component registry."""
1213

1314

14-
@xattree
15+
# kw_only=True necessary so we can define optional fields here
16+
# and required fields in subclasses. attrs complains otherwise
17+
@xattree(kw_only=True)
1518
class Component(ABC, MutableMapping):
1619
"""
1720
Base class for MF6 components.
@@ -22,16 +25,32 @@ class Component(ABC, MutableMapping):
2225
2326
We use the `children` attribute provided by `xattree`. We know
2427
children are also `Component`s, but mypy does not. TODO: fix??
28+
Then we can remove the `# type: ignore` comments.
2529
"""
2630

2731
_load = IO(Loader) # type: ignore
2832
_write = IO(Writer) # type: ignore
2933

34+
filename: str = field(default=None)
35+
36+
@property
37+
def path(self) -> Path:
38+
return Path.cwd() / self.filename
39+
40+
def _default_filename(self) -> str:
41+
name = self.name # type: ignore
42+
cls_name = self.__class__.__name__.lower()
43+
return f"{name}.{cls_name}"
44+
3045
@classmethod
3146
def __attrs_init_subclass__(cls):
3247
COMPONENTS[cls.__name__.lower()] = cls
3348
cls.dfn = cls.get_dfn()
3449

50+
def __attrs_post_init__(self):
51+
if not self.filename:
52+
self.filename = self._default_filename()
53+
3554
def __getitem__(self, key):
3655
return self.children[key] # type: ignore
3756

@@ -49,13 +68,14 @@ def __len__(self):
4968

5069
@classmethod
5170
def get_dfn(cls) -> Dfn:
71+
"""Generate the component's MODFLOW 6 definition."""
5272
fields = {field_name: to_dfn_field(field) for field_name, field in fields_dict(cls).items()}
5373
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
74+
for field_name, field_ in fields.items():
75+
if (block := field_.get("block", None)) is not None:
76+
blocks.setdefault(block, {})[field_name] = field_
5777
else:
58-
blocks[field_name] = field
78+
blocks[field_name] = field_
5979

6080
return Dfn(
6181
name=cls.__name__.lower(),

flopy4/mf6/context.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from abc import ABC
2+
from pathlib import Path
3+
4+
from xattree import xattree
5+
6+
from flopy4.mf6.component import Component
7+
from flopy4.mf6.spec import field
8+
9+
10+
@xattree
11+
class Context(Component, ABC):
12+
workspace: Path = field(default=None)
13+
14+
def __attrs_post_init__(self):
15+
if self.workspace is None:
16+
self.workspace = Path.cwd()
17+
18+
@property
19+
def path(self) -> Path:
20+
return self.workspace / self.filename

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/interface/flopy3.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
from typing import Optional
3+
from warnings import warn
34

45
import numpy as np
56
from flopy.datbase import DataInterface, DataListInterface, DataType
@@ -332,7 +333,7 @@ def data_type(self):
332333
return DataType.array3d
333334
# TODO: boundname, auxvar arrays of strings?
334335
case _:
335-
raise Exception(f"UNMATCHED data_type {self._name}: {self._spec.type.__name__}")
336+
warn(f"UNMATCHED data_type {self._name}: {self._spec.type.__name__}", UserWarning)
336337

337338
@property
338339
def dtype(self):

flopy4/mf6/simulation.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from os import PathLike
2-
from pathlib import Path
3-
from typing import ClassVar
2+
from warnings import warn
43

54
from flopy.discretization.modeltime import ModelTime
65
from modflow_devtools.misc import cd, run_cmd
7-
from xattree import field, xattree
6+
from xattree import xattree
87

9-
from flopy4.mf6.component import Component
8+
from flopy4.mf6.context import Context
109
from flopy4.mf6.exchange import Exchange
1110
from flopy4.mf6.model import Model
1211
from flopy4.mf6.solution import Solution
12+
from flopy4.mf6.spec import field
1313
from flopy4.mf6.tdis import Tdis
1414

1515

@@ -22,36 +22,42 @@ def convert_time(value):
2222

2323

2424
@xattree
25-
class Simulation(Component):
25+
class Simulation(Context):
2626
models: dict[str, Model] = field()
2727
exchanges: dict[str, Exchange] = field()
2828
solutions: dict[str, Solution] = field()
2929
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"
30+
filename: str = field(default="mfsim.nam", init=False)
31+
32+
def __attrs_post_init__(self):
33+
if self.filename != "mfsim.nam":
34+
warn(
35+
"Simulation filename must be 'mfsim.nam'.",
36+
UserWarning,
37+
)
38+
self.filename = "mfsim.nam"
3439

3540
@property
3641
def time(self) -> ModelTime:
42+
"""Return the simulation time discretization."""
3743
return self.tdis.to_time()
3844

3945
def run(self, exe: str | PathLike = "mf6", verbose: bool = False) -> None:
4046
"""Run the simulation using the given executable."""
41-
if self.path is None:
47+
if self.workspace is None:
4248
raise ValueError(f"Simulation {self.name} has no workspace path.")
43-
with cd(self.path):
49+
with cd(self.workspace):
4450
stdout, stderr, retcode = run_cmd(exe, verbose=verbose)
4551
if retcode != 0:
4652
raise RuntimeError(
4753
f"Simulation {self.name}: {exe} failed to run with returncode " # type: ignore
4854
f"{retcode}, and error message:\n\n{stdout + stderr} "
4955
)
5056

51-
def load(self, format):
52-
with cd(self.path):
53-
super().load(format)
57+
def load(self, format="ascii"):
58+
"""Load the simulation in the specified format."""
59+
super().load(format)
5460

55-
def write(self, format):
56-
with cd(self.path):
57-
super().write(format)
61+
def write(self, format="ascii"):
62+
"""Write the simulation in the specified format."""
63+
super().write(format)

0 commit comments

Comments
 (0)