From b1212371bc37b0841c80e7823f29f8aaa1d4ccc6 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sat, 2 Nov 2024 21:46:51 -0400 Subject: [PATCH] misc cleanup --- flopy4/attrs.py | 171 ---------------------------- flopy4/io/lark.py | 25 ----- flopy4/mf6/{ => io}/binary.py | 0 flopy4/mf6/io/converter.py | 3 - flopy4/mf6/io/spec/__init__.py | 4 - flopy4/mf6/io/spec/parser.py | 78 ------------- flopy4/mf6/io/spec/transformer.py | 63 ----------- flopy4/mf6/io/transformer.py | 30 +++-- flopy4/typing.py | 29 +++++ test/attrs/test_gwfic.py | 48 ++++++++ test/{ => attrs}/test_gwfoc.py | 181 +++++++++++++++--------------- test/test_attrs.py | 87 -------------- test/test_lark.py | 75 ------------- 13 files changed, 189 insertions(+), 605 deletions(-) delete mode 100644 flopy4/attrs.py delete mode 100644 flopy4/io/lark.py rename flopy4/mf6/{ => io}/binary.py (100%) delete mode 100644 flopy4/mf6/io/converter.py delete mode 100644 flopy4/mf6/io/spec/__init__.py delete mode 100644 flopy4/mf6/io/spec/parser.py delete mode 100644 flopy4/mf6/io/spec/transformer.py create mode 100644 flopy4/typing.py create mode 100644 test/attrs/test_gwfic.py rename test/{ => attrs}/test_gwfoc.py (52%) delete mode 100644 test/test_attrs.py diff --git a/flopy4/attrs.py b/flopy4/attrs.py deleted file mode 100644 index 87f89ac..0000000 --- a/flopy4/attrs.py +++ /dev/null @@ -1,171 +0,0 @@ -from pathlib import Path -from typing import ( - Any, - Optional, - Tuple, - TypeVar, - Union, -) - -import attr -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.""" - - -Array = ArrayLike -"""An array input parameter""" - - -Table = DataFrame -"""A table input parameter.""" - - -Record = Tuple[Scalar, ...] -"""A record input parameter.""" - - -Param = Union[Scalar, Array, Table, Record] -"""An input parameter.""" - - -# Wrap `attrs.field()` for input parameters. - - -def param( - longname: Optional[str] = None, - description: Optional[str] = None, - deprecated: bool = False, - optional: bool = False, - default=NOTHING, - alias=None, - metadata=None, - validator=None, - converter=None, -): - """ - Define a program input parameter. Wraps `attrs.field()` - with a few extra metadata properties. - """ - metadata = metadata or {} - metadata["longname"] = longname - metadata["description"] = description - metadata["deprecated"] = deprecated - metadata["optional"] = optional - return field( - default=default, - validator=validator, - repr=True, - eq=True, - order=False, - hash=False, - init=True, - alias=alias, - metadata=metadata, - converter=converter, - ) - - -def params(cls): - """ - Return a dictionary of the class' input parameters. - Each parameter is returned as an `attrs.Attribute`. - - Notes - ----- - Wraps `attrs.fields()`. A parameter can be a value - itself or another nested context of parameters. We - eschew the traditional `get_...()` naming in favor - of `params()` in the spirit of `attrs.fields()`. - """ - return {field.name: field for field in fields(cls)} - - -# Wrap `attrs.define()` for input contexts. - - -T = TypeVar("T") - - -def context( - maybe_cls: Optional[type[T]] = None, - *, - auto_attribs: bool = True, - frozen: bool = False, -): - """ - Wrap `attrs.define()` for more opinionated input contexts. - - Notes - ----- - Input contexts may be nested to an arbitrary depth. - - Contexts can be made immutable with `frozen=True`. - """ - - def from_dict(cls, d: dict): - """Convert the dictionary to a context.""" - return structure(d, cls) - - def to_dict(self): - """Convert the context to a dictionary.""" - return asdict(self, recurse=True) - - def wrap(cls): - setattr(cls, "from_dict", classmethod(from_dict)) - setattr(cls, "to_dict", to_dict) - return define( - cls, - auto_attribs=auto_attribs, - frozen=frozen, - slots=False, - weakref_slot=True, - ) - - if maybe_cls is None: - return wrap - - return wrap(maybe_cls) - - -# Utilities - - -def is_attrs(cls: type) -> bool: - """Determines whether the given class is `attrs`-based.""" - - return hasattr(cls, "__attrs_attrs__") - - -def is_frozen(cls: type) -> bool: - """ - Determines whether the `attrs`-based class is frozen (i.e. immutable). - - Notes - ----- - The class *must* be `attrs`-based, otherwise `TypeError` is raised. - - The way to check this may change in the future. See: - - https://github.com/python-attrs/attrs/issues/853 - - https://github.com/python-attrs/attrs/issues/602 - """ - - return cls.__setattr__ == attr._make._frozen_setattrs - - -def to_path(value: Any) -> Optional[Path]: - """Try to convert the value to a `Path`.""" - if value is None: - return None - try: - return Path(value).expanduser() - except: - raise ValueError(f"Can't convert value to Path: {value}") diff --git a/flopy4/io/lark.py b/flopy4/io/lark.py deleted file mode 100644 index de7392f..0000000 --- a/flopy4/io/lark.py +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np - - -def parse_word(self, w): - (w,) = w - return str(w) - - -def parse_string(self, s): - return " ".join(s) - - -def parse_int(self, i): - (i,) = i - return int(i) - - -def parse_float(self, f): - (f,) = f - return float(f) - - -def parse_array(self, a): - (a,) = a - return np.array(a) diff --git a/flopy4/mf6/binary.py b/flopy4/mf6/io/binary.py similarity index 100% rename from flopy4/mf6/binary.py rename to flopy4/mf6/io/binary.py diff --git a/flopy4/mf6/io/converter.py b/flopy4/mf6/io/converter.py deleted file mode 100644 index 78be12c..0000000 --- a/flopy4/mf6/io/converter.py +++ /dev/null @@ -1,3 +0,0 @@ -def make_converter(): - TODO - pass diff --git a/flopy4/mf6/io/spec/__init__.py b/flopy4/mf6/io/spec/__init__.py deleted file mode 100644 index 578901e..0000000 --- a/flopy4/mf6/io/spec/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ["make_parser", "DFNTransformer"] - -from flopy4.mf6.io.spec.parser import make_parser -from flopy4.mf6.io.spec.transformer import DFNTransformer diff --git a/flopy4/mf6/io/spec/parser.py b/flopy4/mf6/io/spec/parser.py deleted file mode 100644 index 39d4e6d..0000000 --- a/flopy4/mf6/io/spec/parser.py +++ /dev/null @@ -1,78 +0,0 @@ -from os import linesep - -from lark import Lark - -ATTRIBUTES = [ - "block", - "name", - "type", - "reader", - "optional", - "true", - "mf6internal", - "longname", - "description", - "layered", - "shape", - "valid", - "tagged", - "in_record", - "preserve_case", - "default_value", - "numeric_index", - "deprecated", -] - -DFN_GRAMMAR = r""" -// dfn -dfn: _NL* (block _NL*)+ _NL* - -// block -block: _header parameter* -_header: _hash _dashes _headtext _dashes _NL+ -_headtext: component subcompnt blockname -component: _word -subcompnt: _word -blockname: _word - -// parameter -parameter.+1: _paramhead _NL (attribute _NL)* -_paramhead: paramblock _NL paramname -paramblock: "block" _word -paramname: "name" _word - -// attribute -attribute.-1: key value -key: ATTRIBUTE -value: string - -// string -_word: /[a-zA-z0-9.;\(\)\-\,\\\/]+/ -string: _word+ - -// newline -_NL: /(\r?\n[\t ]*)+/ - -// comment format -_hash: /\#/ -_dashes: /[\-]+/ - -%import common.SH_COMMENT -> COMMENT -%import common.WORD -%import common.WS_INLINE - -%ignore WS_INLINE -""" -""" -EBNF description for the MODFLOW 6 definition language. -""" - - -def make_parser(): - """ - Create a parser for the MODFLOW 6 definition language. - """ - - attributes = "|".join(['"' + n + '"i' for n in ATTRIBUTES]) - grammar = linesep.join([DFN_GRAMMAR, f"ATTRIBUTE: ({attributes})"]) - return Lark(grammar, start="dfn") diff --git a/flopy4/mf6/io/spec/transformer.py b/flopy4/mf6/io/spec/transformer.py deleted file mode 100644 index 79790fc..0000000 --- a/flopy4/mf6/io/spec/transformer.py +++ /dev/null @@ -1,63 +0,0 @@ -from lark import Transformer - -from flopy4.io.lark import parse_string - - -class DFNTransformer(Transformer): - """ - Transforms a parse tree for the MODFLOW 6 - specification language into a nested AST - suitable for generating an object model. - - Notes - ----- - Rather than a flat list of parameters for each component, - which a subsequent step is responsible for turning into a - an object hierarchy, we derive the hierarchical parameter - structure from the DFN file and return a dict of blocks, - each of which is a dict of parameters. - - This can be fed to a Jinja template to generate component - modules. - """ - - def key(self, k): - (k,) = k - return str(k).lower() - - def value(self, v): - (v,) = v - return str(v) - - def attribute(self, p): - return str(p[0]), str(p[1]) - - def parameter(self, p): - return dict(p[1:]) - - def paramname(self, n): - (n,) = n - return "name", str(n) - - def paramblock(self, b): - (b,) = b - return "block", str(b) - - def component(self, c): - (c,) = c - return "component", str(c) - - def subcompnt(self, s): - (s,) = s - return "subcomponent", str(s) - - def blockname(self, b): - (b,) = b - return "block", str(b) - - def block(self, b): - params = {p["name"]: p for p in b[6:]} - return b[4][1], params - - string = parse_string - dfn = dict diff --git a/flopy4/mf6/io/transformer.py b/flopy4/mf6/io/transformer.py index 2cd4887..47ce6b9 100644 --- a/flopy4/mf6/io/transformer.py +++ b/flopy4/mf6/io/transformer.py @@ -3,13 +3,29 @@ import numpy as np from lark import Transformer -from flopy4.io.lark import ( - parse_array, - parse_float, - parse_int, - parse_string, - parse_word, -) + +def parse_word(_, w): + (w,) = w + return str(w) + + +def parse_string(_, s): + return " ".join(s) + + +def parse_int(_, i): + (i,) = i + return int(i) + + +def parse_float(_, f): + (f,) = f + return float(f) + + +def parse_array(_, a): + (a,) = a + return np.array(a) class MF6Transformer(Transformer): diff --git a/flopy4/typing.py b/flopy4/typing.py new file mode 100644 index 0000000..3ad3f22 --- /dev/null +++ b/flopy4/typing.py @@ -0,0 +1,29 @@ +"""Enumerates supported input variable types.""" + +from pathlib import Path +from typing import ( + Iterable, + Tuple, + Union, +) + +from numpy.typing import ArrayLike + +Scalar = Union[bool, int, float, str, Path] +"""A scalar input variable.""" + + +Array = ArrayLike +"""An array input variable""" + + +Table = Iterable["Record"] +"""A table input variable.""" + + +Record = Tuple[Union[Scalar, "Record"], ...] +"""A record input variable.""" + + +Variable = Union[Scalar, Array, Table, Record] +"""An input variable.""" diff --git a/test/attrs/test_gwfic.py b/test/attrs/test_gwfic.py new file mode 100644 index 0000000..1b8a6a3 --- /dev/null +++ b/test/attrs/test_gwfic.py @@ -0,0 +1,48 @@ +import numpy as np +from attr import define, field +from numpy.typing import NDArray + + +@define +class Options: + export_array_ascii: bool = field( + default=False, + metadata={"longname": "export array variables to netcdf output files"}, + ) + """ + keyword that specifies input griddata arrays should be + written to layered ascii output files. + """ + + export_array_netcdf: bool = field( + default=False, metadata={"longname": "starting head"} + ) + """ + keyword that specifies input griddata arrays should be + written to the model output netcdf file. + """ + + +@define +class PackageData: + strt: NDArray[np.float_] = field( + metadata={"longname": "starting head", "shape": ("nodes")} + ) + """ + is the initial (starting) head---that is, head at the + beginning of the GWF Model simulation. STRT must be specified for + all simulations, including steady-state simulations. One value is + read for every model cell. For simulations in which the first stress + period is steady state, the values used for STRT generally do not + affect the simulation (exceptions may occur if cells go dry and (or) + rewet). The execution time, however, will be less if STRT includes + hydraulic heads that are close to the steady-state solution. A head + value lower than the cell bottom can be provided if a cell should + start as dry. + """ + + +@define +class GwfIc: + options: Options = field() + packagedata: PackageData = field() diff --git a/test/test_gwfoc.py b/test/attrs/test_gwfoc.py similarity index 52% rename from test/test_gwfoc.py rename to test/attrs/test_gwfoc.py index 42caf21..3eeabce 100644 --- a/test/test_gwfoc.py +++ b/test/attrs/test_gwfoc.py @@ -1,101 +1,99 @@ from pathlib import Path -from typing import Dict, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from cattrs import Converter - -from flopy4.attrs import context, is_frozen, param, params, to_path +from attr import asdict, define, field, fields_dict +from cattr import Converter ArrayFormat = Literal["exponential", "fixed", "general", "scientific"] -@context +@define class PrintFormat: - columns: int = param( - description=""" -number of columns for writing data""" - ) - width: int = param( - description=""" -width for writing each number""" - ) - digits: int = param( - description=""" -number of digits to use for writing a number""" - ) - array_format: ArrayFormat = param( - description=""" -write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC""" - ) + columns: int = field() + """ + number of columns for writing data + """ + width: int = field() + """ + width for writing each number + """ -@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, - ) + digits: int = field() + """ + number of digits to use for writing a number + """ + array_format: ArrayFormat = field() + """ + write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC + """ -@context + +@define +class Options: + budget_file: Optional[Path] = field(default=None) + """ + name of the output file to write budget information + """ + + budget_csv_file: Optional[Path] = field(default=None) + """ + 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. + """ + + head_file: Optional[Path] = field(default=None) + """ + name of the output file to write head information. + """ + + print_format: Optional[PrintFormat] = field(default=None) + """ + specify format for printing to the listing file + """ + + +@define class All: - all: bool = param( - description=""" -keyword to indicate save for all time steps in period.""" - ) + all: bool = field() + """ + keyword to indicate save for all time steps in period. + """ -@context +@define class First: - first: bool = param( - description=""" -keyword to indicate save for first step in period.""" - ) + first: bool = field() + """ + keyword to indicate save for first step in period. + """ -@context +@define class Last: - last: bool = param( - description=""" -keyword to indicate save for last step in period""" - ) + last: bool = field() + """ + keyword to indicate save for last step in period + """ -@context +@define class Steps: - steps: List[int] = param( - description=""" -save for each step specified.""" - ) + steps: List[int] = field() + """ + save for each step specified + """ -@context +@define class Frequency: - frequency: int = param( - description=""" -save at the specified time step frequency.""" - ) + frequency: int = field() + """ + save at the specified time step frequency. + """ # It's awkward to have single-parameter contexts, but @@ -108,11 +106,11 @@ class Frequency: OCSetting = Union[All, First, Last, Steps, Frequency] -@context +@define class OutputControlData: - printsave: PrintSave = param() - rtype: RType = param() - ocsetting: OCSetting = param() + printsave: PrintSave = field() + rtype: RType = field() + ocsetting: OCSetting = field() @classmethod def from_tuple(cls, t): @@ -133,16 +131,17 @@ def from_tuple(cls, t): Periods = List[Period] -@context +@define class GwfOc: - options: Options = param( - description=""" -options block""" - ) - periods: Periods = param( - description=""" -period blocks""" - ) + options: Options = field() + """ + options block + """ + + periods: Periods = field() + """ + period blocks + """ # Converter @@ -161,10 +160,8 @@ def output_control_data_hook(value, type) -> OutputControlData: def test_spec(): - spec = params(OutputControlData) + spec = fields_dict(OutputControlData) assert len(spec) == 3 - assert isinstance(spec, Dict) - assert not is_frozen(OutputControlData) ocsetting = spec["ocsetting"] assert ocsetting.type is OCSetting @@ -174,8 +171,8 @@ def test_options_to_dict(): options = Options( budget_file="some/file/path.cbc", ) - assert isinstance(options.budget_file, Path) - assert len(options.to_dict()) == 4 + assert isinstance(options.budget_file, str) # TODO path + assert len(asdict(options)) == 4 def test_output_control_data_from_tuple(): diff --git a/test/test_attrs.py b/test/test_attrs.py deleted file mode 100644 index 80c8649..0000000 --- a/test/test_attrs.py +++ /dev/null @@ -1,87 +0,0 @@ -import math -from pathlib import Path -from typing import Dict, Optional - -import numpy as np -import pytest -from attrs import asdict, astuple -from numpy.typing import NDArray - -from flopy4.attrs import ( - context, - is_frozen, - param, - params, -) - - -@context(frozen=True) -class Record: - rb: bool = param(description="bool in record") - ri: int = param(description="int in record") - rf: float = param(description="float in record") - rs: Optional[str] = param( - description="optional str in record", default=None - ) - - -@context -class Block: - b: bool = param(description="bool") - i: int = param(description="int") - f: float = param(description="float") - s: str = param(description="str", optional=False) - p: Path = param(description="path", optional=False) - a: NDArray[np.int_] = param(description="array") - r: Record = param( - description="record", - optional=False, - ) - - -def test_spec(): - spec = params(Record) - assert len(spec) == 4 - assert isinstance(spec, Dict) - assert is_frozen(Record) - - spec = params(Block) - assert len(spec) == 7 - assert isinstance(spec, Dict) - assert not is_frozen(Block) - - b = spec["b"] - assert b.type is bool - assert b.metadata["description"] == "bool" - - i = spec["i"] - assert i.type is int - assert i.metadata["description"] == "int" - - f = spec["f"] - assert f.type is float - assert f.metadata["description"] == "float" - - s = spec["s"] - assert s.type is str - assert s.metadata["description"] == "str" - - p = spec["p"] - assert p.type is Path - assert p.metadata["description"] == "path" - - a = spec["a"] - assert a.type == NDArray[np.int_] - assert a.metadata["description"] == "array" - - r = spec["r"] - assert r.type is Record - assert r.metadata["description"] == "record" - - -def test_usage(): - r = Record(rb=True, ri=42, rf=math.pi) - assert astuple(r) == (True, 42, math.pi, None) - assert asdict(r) == {"rb": True, "ri": 42, "rf": math.pi, "rs": None} - with pytest.raises(TypeError): - Record(rb=True) diff --git a/test/test_lark.py b/test/test_lark.py index f270e64..b6d3557 100644 --- a/test/test_lark.py +++ b/test/test_lark.py @@ -5,8 +5,6 @@ from flopy4.mf6.io import MF6Transformer from flopy4.mf6.io import make_parser as make_mf6_parser -from flopy4.mf6.io.spec import DFNTransformer -from flopy4.mf6.io.spec import make_parser as make_dfn_parser COMPONENT = """ BEGIN OPTIONS @@ -61,76 +59,3 @@ def test_transform_mf6(): assert data["period 1"][0] == ("FIRST",) assert data["period 1"][1] == ("FREQUENCY", 2) assert data["period 2"][0] == ("STEPS", 1, 2, 3) - - -DFN_PARSER = make_dfn_parser() -DFN_TRANSFORMER = DFNTransformer() - -PROJ_ROOT = Path(__file__).parents[1] -DFNS_PATH = PROJ_ROOT / "spec" / "dfn" -DFN_PATH = DFNS_PATH / "gwf-ic.dfn" - - -def test_parse_dfn(): - tree = DFN_PARSER.parse(open(DFN_PATH).read()) - print(tree.pretty()) - - -def test_transform_dfn(): - tree = DFN_PARSER.parse(open(DFN_PATH).read()) - data = DFN_TRANSFORMER.transform(tree) - assert data["options"] == { - "export_array_ascii": { - "description": "keyword that specifies " - "input griddata arrays " - "should be written to " - "layered ascii output " - "files.", - "longname": "export array variables to " "layered ascii files.", - "mf6internal": "export_ascii", - "name": "export_array_ascii", - "optional": "true", - "reader": "urword", - "type": "keyword", - }, - "export_array_netcdf": { - "description": "keyword that specifies " - "input griddata arrays " - "should be written to the " - "model output netcdf file.", - "longname": "export array variables to " "netcdf output files.", - "mf6internal": "export_nc", - "name": "export_array_netcdf", - "optional": "true", - "reader": "urword", - "type": "keyword", - }, - } - assert data["griddata"] == { - "strt": { - "default_value": "1.0", - "description": "is the initial (starting) head---that " - "is, head at the beginning of the GWF " - "Model simulation. STRT must be " - "specified for all simulations, " - "including steady-state simulations. One " - "value is read for every model cell. For " - "simulations in which the first stress " - "period is steady state, the values used " - "for STRT generally do not affect the " - "simulation (exceptions may occur if " - "cells go dry and (or) rewet). The " - "execution time, however, will be less " - "if STRT includes hydraulic heads that " - "are close to the steady-state solution. " - "A head value lower than the cell bottom " - "can be provided if a cell should start " - "as dry.", - "layered": "true", - "longname": "starting head", - "name": "strt", - "reader": "readarray", - "shape": "(nodes)", - "type": "double precision", - } - }