Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions flopy4/mf6/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pathlib import Path

from flopy4.mf6.codec import dump, load
from flopy4.mf6.component import Component
from flopy4.uio import DEFAULT_REGISTRY


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


def _path(component: Component) -> str:
"""Default path for a component, based on its name."""
if hasattr(component, "path") and component.path is not None:
path = Path(component.path).expanduser().resolve()
if path.is_dir():
return str(path / _default_filename(component))
return str(path)
return _default_filename(component)


DEFAULT_REGISTRY.register_loader(Component, "ascii", lambda component: load(_path(component)))
DEFAULT_REGISTRY.register_writer(
Component, "ascii", lambda component: dump(component, _path(component))
)
16 changes: 5 additions & 11 deletions flopy4/mf6/codec.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import sys
from os import PathLike

import numpy as np
from jinja2 import Environment, PackageLoader

from flopy4.mf6 import filters
from flopy4.mf6.component import Component
from flopy4.uio import DEFAULT_REGISTRY

JINJA_ENV = Environment(
loader=PackageLoader("flopy4.mf6"),
Expand All @@ -21,21 +20,16 @@
JINJA_TEMPLATE_NAME = "blocks.jinja"


def _load_ascii(self) -> None:
def load(path: str | PathLike) -> None:
# TODO
pass


def _write_ascii(self) -> None:
def dump(data, path: str | PathLike) -> None:
template = JINJA_ENV.get_template(JINJA_TEMPLATE_NAME)
iterator = template.generate(dfn=type(self).dfn, data=self)
iterator = template.generate(dfn=type(data).dfn, data=data)
# are these printoptions always applicable?
with np.printoptions(precision=4, linewidth=sys.maxsize, threshold=sys.maxsize):
# TODO don't hardcode the filename, maybe a filename attribute?
with open(self.path / self.name, "w") as f: # type: ignore
with open(path, "w") as f: # type: ignore
f.writelines(iterator)


# TODO: where to do this? probably not here..on plugin discovery?
DEFAULT_REGISTRY.register_loader(Component, "ascii", _load_ascii)
DEFAULT_REGISTRY.register_writer(Component, "ascii", _write_ascii)
8 changes: 4 additions & 4 deletions flopy4/mf6/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ def get_dfn(cls) -> Dfn:
**blocks,
)

def load(self) -> None:
def load(self, format: str) -> None:
self._load(format=format)
for child in self.children.values(): # type: ignore
child.load()
child.load(format)

def write(self) -> None:
def write(self, format: str) -> None:
self._write(format=format)
for child in self.children.values(): # type: ignore
child.write()
child.write(format)
14 changes: 12 additions & 2 deletions flopy4/mf6/simulation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from os import PathLike
from pathlib import Path
from typing import ClassVar

from flopy.discretization.modeltime import ModelTime
from modflow_devtools.misc import run_cmd, set_dir
from modflow_devtools.misc import cd, run_cmd
from xattree import field, xattree

from flopy4.mf6.component import Component
Expand All @@ -29,6 +30,7 @@ class Simulation(Component):
# TODO: decorator for components bound
# to some directory or file path?
path: Path = field(default=None)
filename: ClassVar[str] = "mfsim.nam"

@property
def time(self) -> ModelTime:
Expand All @@ -38,10 +40,18 @@ def run(self, exe: str | PathLike = "mf6", verbose: bool = False) -> None:
"""Run the simulation using the given executable."""
if self.path is None:
raise ValueError(f"Simulation {self.name} has no workspace path.")
with set_dir(self.path):
with cd(self.path):
stdout, stderr, retcode = run_cmd(exe, verbose=verbose)
if retcode != 0:
raise RuntimeError(
f"Simulation {self.name}: {exe} failed to run with returncode " # type: ignore
f"{retcode}, and error message:\n\n{stdout + stderr} "
)

def load(self, format):
with cd(self.path):
super().load(format)

def write(self, format):
with cd(self.path):
super().write(format)
26 changes: 14 additions & 12 deletions flopy4/uio.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def get_loader(self, cls, format=None):
iter(
[
fn
for ((fmt, cls_), fn) in self._loaders.items()
for (cls_, fmt), fn in self._loaders.items()
if fmt == format and issubclass(cls, cls_)
]
)
Expand All @@ -39,7 +39,7 @@ def get_writer(self, cls, format=None):
iter(
[
fn
for ((fmt, cls_), fn) in self._writers.items()
for (cls_, fmt), fn in self._writers.items()
if fmt == format and issubclass(cls, cls_)
]
)
Expand All @@ -48,18 +48,20 @@ def get_writer(self, cls, format=None):
def register_loader(self, cls, format, function):
if format in self._loaders:
raise ValueError(f"Loader for format {format} already registered.")
self._loaders[cls, format] = (cls, function)
self._loaders[cls, format] = function

def register_writer(self, cls, format, function):
if format in self._writers:
raise ValueError(f"Writer for format {format} already registered.")
self._writers[cls, format] = (cls, function)
self._writers[cls, format] = function

def load(self, cls, *args, format=None, **kwargs):
return self.get_loader(cls, format)(*args, **kwargs)
def load(self, cls, instance, *args, format=None, **kwargs):
_load = self.get_loader(cls, format)
_load(instance, *args, **kwargs)

def write(self, cls, *args, format=None, **kwargs):
return self.get_writer(cls, format)(*args, **kwargs)
def write(self, cls, instance, *args, format=None, **kwargs):
_write = self.get_writer(cls, format)
_write(instance, *args, **kwargs)


DEFAULT_REGISTRY = Registry()
Expand Down Expand Up @@ -103,17 +105,17 @@ class Loader(IODescriptor):
"""Descriptor for loading data from file."""

def __init__(self, instance, cls):
super().__init__(instance, cls, "load", registry=None)
super().__init__(instance, cls, "load", registry=DEFAULT_REGISTRY)

def __call__(self, *args, **kwargs) -> None:
return self.registry.load(self._cls, *args, **kwargs)
return self.registry.load(self._cls, self._instance, *args, **kwargs)


class Writer(IODescriptor):
"""Descriptor for writing data to file."""

def __init__(self, instance, cls):
super().__init__(instance, cls, "write", registry=None)
super().__init__(instance, cls, "write", registry=DEFAULT_REGISTRY)

def __call__(self, *args, **kwargs) -> None:
return self.registry.write(self._cls, *args, **kwargs)
return self.registry.write(self._cls, self._instance, *args, **kwargs)
17 changes: 9 additions & 8 deletions test/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,19 @@ def test_ims_dfn():
assert "inner_maximum" in set(dfn["linear"].keys())


@pytest.mark.xfail(reason="TODO")
def test_write_ascii(tmp_path):
time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0])
grid = StructuredGrid(nlay=1, nrow=10, ncol=10)
sim = Simulation(tdis=time, path=tmp_path)
gwf = Gwf(parent=sim, dis=grid)
ic = Ic(parent=gwf)
oc = Oc(parent=gwf)
npf = Npf(parent=gwf)
chd = Chd(parent=gwf, head={"*": {(0, 0, 0): 1.0, (0, 9, 9): 0.0}})
# TODO fix errors
# gwf = Gwf(parent=sim, dis=grid)
# ic = Ic(parent=gwf)
# oc = Oc(parent=gwf)
# npf = Npf(parent=gwf)
# chd = Chd(parent=gwf, head={"*": {(0, 0, 0): 1.0, (0, 9, 9): 0.0}})

sim.write("ascii")

files = Path(tmp_path).glob("*")
assert "mfsim.nam" in files
files = list(Path(tmp_path).glob("*"))
file_names = [f.name for f in files]
assert "mfsim.nam" in file_names
Loading