Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into develop
  • Loading branch information
mjr-deltares committed Sep 5, 2024
2 parents 631a95e + e97e06b commit dded4cb
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 75 deletions.
35 changes: 11 additions & 24 deletions flopy4/attrs.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from pathlib import Path
from typing import (
Any,
Optional,
TypeVar,
Union,
)

import attr
from attrs import NOTHING, define, field, fields
from cattrs import structure, unstructure
from attrs import NOTHING, asdict, define, field, fields
from cattrs import structure
from numpy.typing import ArrayLike
from pandas import DataFrame

# Enumerate the primitive types to support.
# This is just for reference, not meant to
# be definitive, exclusive, or exhaustive.

Scalar = Union[bool, int, float, str, Path]
"""A scalar input parameter."""
Expand Down Expand Up @@ -109,7 +112,7 @@ def from_dict(cls, d: dict):

def to_dict(self):
"""Convert the context to a dictionary."""
return unstructure(self)
return asdict(self, recurse=True)

def wrap(cls):
setattr(cls, "from_dict", classmethod(from_dict))
Expand All @@ -128,23 +131,6 @@ def wrap(cls):
return wrap(maybe_cls)


def choice(
maybe_cls: Optional[type[T]] = None,
*,
frozen: bool = False,
):
def wrap(cls):
return context(
cls,
frozen=frozen,
)

if maybe_cls is None:
return wrap

return wrap(maybe_cls)


# Utilities


Expand All @@ -170,10 +156,11 @@ def is_frozen(cls: type) -> bool:
return cls.__setattr__ == attr._make._frozen_setattrs


def to_path(val) -> Optional[Path]:
if val is None:
def to_path(value: Any) -> Optional[Path]:
"""Try to convert the value to a `Path`."""
if value is None:
return None
try:
return Path(val).expanduser()
return Path(value).expanduser()
except:
raise ValueError(f"Cannot convert value to Path: {val}")
raise ValueError(f"Can't convert value to Path: {value}")
Empty file added flopy4/converter.py
Empty file.
68 changes: 68 additions & 0 deletions flopy4/lark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from lark import Lark

MF6_GRAMMAR = r"""
?start: _NL* _item*
_item: (block | COMMENT) _NL+
// block
block: _begin _NL params _end
_begin: _BEGIN name [index]
_end: _END name
name: WORD
index: INT
_BEGIN: "begin"i
_END: "end"i
// parameter
params: (param _NL)*
param: _key [_value]
_key: KEYS
_value: NUMBER | path | string | array | list
// string
string: WORD+
// file path
path: INOUT PATH
PATH: [_PATHSEP] (NON_SEPARATOR_STRING [_PATHSEP]) [NON_SEPARATOR_STRING]
_PATHSEP: "/"
INOUT: "filein"i|"fileout"i
// array
array: constantarray | internalarray | externalarray
constantarray: "CONSTANT" NUMBER
internalarray: "INTERNAL" [factor] [iprn] (NUMBER* [_NL])*
externalarray: "OPEN/CLOSE" WORD [factor] ["binary"] [iprn]
factor: "FACTOR" NUMBER
iprn: "IPRN" INT
// list (adapted from https://github.com/lark-parser/lark/blob/master/examples/composition/csv.lark)
list: header _NL row*
header: "#" " "? (WORD _SEPARATOR?)+
row: (_anything _SEPARATOR?)+ _NL
_anything: INT | WORD | NON_SEPARATOR_STRING | FLOAT | SIGNED_FLOAT
NON_SEPARATOR_STRING: /[a-zA-z.;\\\/]+/
_SEPARATOR: /[ ]+/
| "\t"
| ","
// newline
_NL: /(\r?\n[\t ]*)+/
// parameter keys file can be generated
// with the rest of the plugin interface
// and maybe placed in a separate file
KEYS: "K"|"I"|"D"|"S"|"F"|"A"
%import common.SH_COMMENT -> COMMENT
%import common.SIGNED_NUMBER -> NUMBER
%import common.SIGNED_FLOAT
%import common.INT
%import common.FLOAT
%import common.WORD
%import common.WS_INLINE
%ignore WS_INLINE
"""

MF6_PARSER = Lark(MF6_GRAMMAR, start="start")
2 changes: 0 additions & 2 deletions flopy4/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ class MFParamSpec:
repeating: bool = False
tagged: bool = True
reader: MFReader = MFReader.urword
# todo change to variadic tuple of str and resolve
# actual shape at load time from simulation context
shape: Optional[Tuple[int]] = None
default_value: Optional[Any] = None

Expand Down
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ classifiers = [
]
requires-python = ">=3.9"
dependencies = [
"attrs", # todo: lower bound?
"cattrs", # todo: lower bound?
"Jinja2>=3.0",
"attrs", # todo: bounds?
"cattrs", # todo: bounds?
"flopy>=3.7.0",
"Jinja2>=3.0",
"lark", # todo: bounds?
"numpy>=1.20.3",
"pandas>=2.0.0",
"toml>=0.10",
Expand All @@ -54,6 +55,7 @@ test = [
"flopy4[lint]",
"coverage",
"GitPython",
"interegular",
"jupyter",
"jupytext",
"modflow-devtools",
Expand Down
7 changes: 0 additions & 7 deletions test/test_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
params,
)

# Records are product types: named, ordered tuples of scalars.
# Records are immutable: they can't be changed, only evolved.


@context(frozen=True)
class Record:
Expand All @@ -42,9 +39,6 @@ class Block:
)


# Keystrings are sum types: discriminated unions of records.


def test_spec():
spec = params(Record)
assert len(spec) == 4
Expand Down Expand Up @@ -90,5 +84,4 @@ def test_usage():
assert astuple(r) == (True, 42, math.pi, None)
assert asdict(r) == {"rb": True, "ri": 42, "rf": math.pi, "rs": None}
with pytest.raises(TypeError):
# non-optional members are required
Record(rb=True)
95 changes: 56 additions & 39 deletions test/test_gwfoc.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
from pathlib import Path
from typing import Dict, List, Literal, Optional, Union

from cattrs import unstructure
import pytest

from flopy4.attrs import context, is_frozen, param, params, to_path

# Define the package input specification.
# Some of this will be generic, and come
# from elsewhere, eventually.

ArrayFormat = Literal["exponential", "fixed", "general", "scientific"]


Expand All @@ -32,6 +28,36 @@ class PrintFormat:
)


@context
class Options:
budget_file: Optional[Path] = param(
description="""
name of the output file to write budget information""",
converter=to_path,
default=None,
)
budget_csv_file: Optional[Path] = param(
description="""
name of the comma-separated value (CSV) output
file to write budget summary information.
A budget summary record will be written to this
file for each time step of the simulation.""",
converter=to_path,
default=None,
)
head_file: Optional[Path] = param(
description="""
name of the output file to write head information.""",
converter=to_path,
default=None,
)
print_format: Optional[PrintFormat] = param(
description="""
specify format for printing to the listing file""",
default=None,
)


@context
class All:
all: bool = param(
Expand Down Expand Up @@ -74,7 +100,7 @@ class Frequency:

# It's awkward to have single-parameter contexts, but
# it's the only way I got `cattrs` to distinguish the
# choices in the union.
# choices in the union. There is likely a better way.


StepSelection = Union[All, First, Last, Steps, Frequency]
Expand All @@ -89,36 +115,6 @@ class OutputControlData:
ocsetting: StepSelection = param()


@context
class Options:
budget_file: Optional[Path] = param(
description="""
name of the output file to write budget information""",
converter=to_path,
default=None,
)
budget_csv_file: Optional[Path] = param(
description="""
name of the comma-separated value (CSV) output
file to write budget summary information.
A budget summary record will be written to this
file for each time step of the simulation.""",
converter=to_path,
default=None,
)
head_file: Optional[Path] = param(
description="""
name of the output file to write head information.""",
converter=to_path,
default=None,
)
print_format: Optional[PrintFormat] = param(
description="""
specify format for printing to the listing file""",
default=None,
)


Period = List[OutputControlData]
Periods = List[Period]

Expand Down Expand Up @@ -148,15 +144,36 @@ def test_spec():
assert ocsetting.type is StepSelection


def test_options():
def test_options_to_dict():
options = Options(
budget_file="some/file/path.cbc",
)
assert isinstance(options.budget_file, Path)
assert len(unstructure(options)) == 4
assert len(options.to_dict()) == 4


def test_output_control_data_from_dict():
# from dict
ocdata = OutputControlData.from_dict(
{
"action": "print",
"variable": "budget",
"ocsetting": {"steps": [1, 3, 5]},
}
)
assert ocdata.action == "print"


@pytest.mark.xfail(reason="todo")
def test_output_control_data_from_tuple():
ocdata = OutputControlData.from_tuple(
("print", "budget", "steps", 1, 3, 5)
)
assert ocdata.action == "print"
assert ocdata.variable == "budget"


def test_gwfoc_structure():
def test_gwfoc_from_dict():
gwfoc = GwfOc.from_dict(
{
"options": {
Expand Down
42 changes: 42 additions & 0 deletions test/test_lark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pprint import pprint

import pytest
from lark import Transformer

from flopy4.lark import MF6_PARSER

TEST_PKG = """
BEGIN OPTIONS
K
I 1
D 1.0
S hello world
F FILEIN some/path
END OPTIONS
BEGIN PACKAGEDATA 1
A INTERNAL 1.0 2.0 3.0
END PACKAGEDATA
"""


def test_parse_mf6():
tree = MF6_PARSER.parse(TEST_PKG)
# this is working, check it with:
# pytest test/test_lark.py::test_parse_mf6 -s
print(tree.pretty())


class MF6Transformer(Transformer):
# TODO
pass


MF6_TRANSFORMER = MF6Transformer()


@pytest.mark.xfail
def test_transform_mf6():
tree = MF6_PARSER.parse(TEST_PKG)
data = MF6_TRANSFORMER.transform(tree)
pprint(data)

0 comments on commit dded4cb

Please sign in to comment.