Skip to content

Commit

Permalink
Merge pull request #8 from wpbonelli/compound
Browse files Browse the repository at this point in the history
Compound parameters
  • Loading branch information
wpbonelli authored Jul 24, 2024
2 parents 494a0da + c9e907a commit 8f82fb9
Show file tree
Hide file tree
Showing 11 changed files with 520 additions and 100 deletions.
8 changes: 5 additions & 3 deletions flopy4/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flopy.utils.flopy_io import line_strip, multi_line_strip

from flopy4.constants import CommonNames
from flopy4.parameter import MFParameter, MFReader
from flopy4.param import MFParam, MFReader


class NumPyArrayMixin:
Expand Down Expand Up @@ -174,7 +174,7 @@ def from_string(cls, string):
return e


class MFArray(MFParameter, NumPyArrayMixin):
class MFArray(MFParam, NumPyArrayMixin):
"""
A MODFLOW 6 array backed by a 1-dimensional NumPy array,
which is reshaped as needed for various views. Supports
Expand All @@ -189,6 +189,7 @@ def __init__(
factor=None,
block=None,
name=None,
type=None,
longname=None,
description=None,
deprecated=False,
Expand All @@ -202,10 +203,11 @@ def __init__(
reader=MFReader.urword,
default_value=None,
):
MFParameter.__init__(
MFParam.__init__(
self,
block=block,
name=name,
type=type,
longname=longname,
description=description,
deprecated=deprecated,
Expand Down
94 changes: 69 additions & 25 deletions flopy4/block.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
from abc import ABCMeta
from collections import UserDict
from dataclasses import asdict
from io import StringIO
from typing import Any

from flopy4.array import MFArray
from flopy4.parameter import MFParameter, MFParameters
from flopy4.utils import strip
from flopy4.compound import MFKeystring, MFRecord
from flopy4.param import MFParam, MFParams
from flopy4.utils import find_upper, strip


def single_keystring(members):
params = list(members.values())
return len(params) == 1 and isinstance(params[0], MFKeystring)


def get_param(members, key, block):
ks = [m for m in members.values() if isinstance(m, MFKeystring)]
if len(ks) == 1:
param = ks[0]
else:
param = members.get(key)
param.name = key
param.block = block
return param


class MFBlockMeta(type):
Expand All @@ -14,14 +32,25 @@ def __new__(cls, clsname, bases, attrs):
if clsname == "MFBlock":
return new

# add parameter specification as class attribute
new.params = MFParameters(
{
k: v
for k, v in attrs.items()
if issubclass(type(v), MFParameter)
}
# detect block name
block_name = (
clsname[list(find_upper(clsname))[1] :]
.replace("Block", "")
.lower()
)

# add parameter specification class attributes
params = {
k: v.with_name(k).with_block(block_name)
for k, v in attrs.items()
if issubclass(type(v), MFParam)
}
if len([p for p in params if isinstance(p, MFKeystring)]) > 1:
raise ValueError("Only one keystring allowed per block")
for key, param in params.items():
setattr(new, key, param)
new.params = MFParams(params)

return new


Expand All @@ -30,7 +59,7 @@ class MFBlockMappingMeta(MFBlockMeta, ABCMeta):
pass


class MFBlock(MFParameters, metaclass=MFBlockMappingMeta):
class MFBlock(MFParams, metaclass=MFBlockMappingMeta):
"""
MF6 input block. Maps parameter names to parameters.
Expand All @@ -50,22 +79,26 @@ def __init__(self, name=None, index=None, params=None):
self.name = name
self.index = index
super().__init__(params)
for key, param in self.items():
setattr(self, key, param)

def __getattribute__(self, name: str) -> Any:
# shortcut to parameter value for instance attribute.
# the class attribute is the full parameter instance.
attr = super().__getattribute__(name)
return attr.value if isinstance(attr, MFParameter) else attr
return attr.value if isinstance(attr, MFParam) else attr

def __str__(self):
buffer = StringIO()
self.write(buffer)
return buffer.getvalue()

@property
def params(self) -> MFParameters:
def params(self) -> MFParams:
"""Block parameters."""
return self.data

@classmethod
def load(cls, f, **kwargs):
"""Load the block from file."""
name = None
index = None
found = False
Expand All @@ -74,38 +107,47 @@ def load(cls, f, **kwargs):

while True:
pos = f.tell()
line = strip(f.readline()).lower()
words = line.split()
line = f.readline()
if line == "":
raise ValueError("Early EOF, aborting")
if line == "\n":
continue
words = strip(line).lower().split()
key = words[0]
if key == "begin":
found = True
name = words[1]
if len(words) > 2 and str.isdigit(words[2]):
index = words[2]
index = int(words[2])
elif key == "end":
break
elif found:
param = members.get(key)
param = get_param(members, key, name)
if param is not None:
f.seek(pos)
spec = asdict(param.with_name(key).with_block(name))
kwargs = {**kwargs, **spec}
if type(param) is MFArray:
spec = asdict(param)
kwrgs = {**kwargs, **spec}
ptype = type(param)
if ptype is MFArray:
# TODO: inject from model somehow?
# and remove special handling here
kwargs["cwd"] = ""
params[key] = type(param).load(f, **kwargs)
kwrgs["cwd"] = ""
if ptype is MFRecord:
kwrgs["params"] = param.data.copy()
if ptype is MFKeystring:
kwrgs["params"] = param.data.copy()
params[param.name] = ptype.load(f, **kwrgs)

return cls(name, index, params)

def write(self, f):
"""Write the block to file."""
index = self.index if self.index is not None else ""
begin = f"BEGIN {self.name.upper()} {index}\n"
end = f"END {self.name.upper()}\n"

f.write(begin)
for param in self.values():
param.write(f)
super().write(f)
f.write(end)


Expand All @@ -114,3 +156,5 @@ class MFBlocks(UserDict):

def __init__(self, blocks=None):
super().__init__(blocks)
for key, block in self.items():
setattr(self, key, block)
Loading

0 comments on commit 8f82fb9

Please sign in to comment.