Skip to content

Commit ed2c3af

Browse files
authored
array reader (#178)
Step towards a lark reader with more knowledge of the MF6 input format. Just arrays for now, lists to come shortly. The keyword name is omitted — thinking we can define generic rules for MF6 input format type system which can then be consumed by component-specific generated grammars. Might move this to devtools at some point, not sure yet.
1 parent ef021f8 commit ed2c3af

File tree

9 files changed

+817
-533
lines changed

9 files changed

+817
-533
lines changed

flopy4/mf6/codec/reader/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from pathlib import Path
33
from typing import Any
44

5-
from flopy4.mf6.codec.reader.parser import make_generic_parser
6-
from flopy4.mf6.codec.reader.transformer import GenericTransformer
5+
from flopy4.mf6.codec.reader.parser import make_basic_parser
6+
from flopy4.mf6.codec.reader.transformer import BasicTransformer
77

88

99
def load(path: str | PathLike) -> Any:
@@ -41,6 +41,6 @@ def loads(data: str) -> Any:
4141
Parsed MF6 input file structure
4242
"""
4343

44-
parser = make_generic_parser()
45-
transformer = GenericTransformer()
44+
parser = make_basic_parser()
45+
transformer = BasicTransformer()
4646
return transformer.transform(parser.parse(data))

flopy4/mf6/codec/reader/grammar/__init__.py

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
start: readarray
2+
readarray: (single_array | layered_array)
3+
single_array: [netcdf] array
4+
layered_array: layered [netcdf] array+
5+
layered: "layered"i
6+
netcdf: "netcdf"i
7+
array: control [data]
8+
control: constant | internal | external
9+
constant: "constant"i _number
10+
internal: "internal"i [factor] [iprn]
11+
external: "open/close"i filename [factor] [binary] [iprn]
12+
factor: "factor"i _number
13+
iprn: "iprn"i _signed_integer
14+
binary: "(" "binary"i ")"
15+
filename: ESCAPED_STRING | _word
16+
data: _number+
17+
_number: SIGNED_NUMBER | NUMBER
18+
_signed_integer: SIGNED_INT | INT
19+
_word: /[a-zA-Z0-9._'~,-\\(\\)]+/
20+
21+
%import common.NEWLINE -> _NL
22+
%import common.WS
23+
%import common.WS_INLINE
24+
%import common.CNAME
25+
%import common.WORD
26+
%import common.NUMBER
27+
%import common.INT
28+
%import common.SH_COMMENT
29+
%import common.ESCAPED_STRING
30+
%import common.SIGNED_NUMBER
31+
%import common.SIGNED_INT
32+
33+
%ignore WS
34+
%ignore SH_COMMENT

flopy4/mf6/codec/reader/grammar/mf6.lark renamed to flopy4/mf6/codec/reader/grammar/basic.lark

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ word: /[a-zA-Z0-9._'~,-\\(\\)]+/
1515
%import common.INT
1616
%import common.SH_COMMENT
1717
%import common._STRING_INNER
18+
1819
%ignore WS_INLINE
1920
%ignore SH_COMMENT

flopy4/mf6/codec/reader/parser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
from lark import Lark
44

55

6-
def make_generic_parser() -> Lark:
7-
grammar_path = Path(__file__).parent / "grammar" / "mf6.lark"
6+
def make_basic_parser() -> Lark:
7+
grammar_path = Path(__file__).parent / "grammar" / "basic.lark"
8+
with open(grammar_path, "r") as f:
9+
grammar = f.read()
10+
return Lark(grammar, parser="lalr", debug=True)
11+
12+
13+
def make_array_parser() -> Lark:
14+
grammar_path = Path(__file__).parent / "grammar" / "array.lark"
815
with open(grammar_path, "r") as f:
916
grammar = f.read()
1017
return Lark(grammar, parser="lalr", debug=True)

flopy4/mf6/codec/reader/transformer.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
from pathlib import Path
12
from typing import Any
23

4+
import numpy as np
5+
import xarray as xr
36
from lark import Token, Transformer
47

58

6-
class GenericTransformer(Transformer):
9+
class BasicTransformer(Transformer):
710
"""
8-
Generic transformer for MF6 input files. Works only with the generic
9-
grammar. Returns structures of blocks consisting of lines of tokens.
11+
Basic transformer for MF6 input files. Works only with the basic
12+
grammar. Yields blocks simply as collections of lines of tokens.
1013
"""
1114

1215
def start(self, items: list[Any]) -> dict[str, Any]:
@@ -48,3 +51,123 @@ def CNAME(self, token: Token) -> str:
4851

4952
def INT(self, token: Token) -> int:
5053
return int(token)
54+
55+
56+
class ArrayTransformer(Transformer):
57+
"""
58+
Transformer for MF6 array input format. Returns xarray DataArrays
59+
for internal/constant arrays and Path objects for external arrays,
60+
inside a dictionary which also contains control information. This
61+
is a first step towards a smarter parser/transformer for the full
62+
MF6 input format specification.
63+
"""
64+
65+
def start(self, items: list[Any]) -> dict:
66+
return items[0]
67+
68+
def readarray(self, items: list[Any]) -> dict:
69+
infos = items[0]
70+
if isinstance(infos, list):
71+
data = xr.concat([info["data"] for info in infos if "data" in info], dim="layer")
72+
return {
73+
"control": [info["control"] for info in infos if "control" in info],
74+
"data": data,
75+
"attrs": {k: v for k, v in infos[0].items() if k not in ["data"]},
76+
"dims": {"layer": len(infos)},
77+
}
78+
return infos
79+
80+
def single_array(self, items: list[Any]) -> dict:
81+
netcdf = items[0]
82+
info = items[-1]
83+
if netcdf:
84+
info["netcdf"] = netcdf
85+
return ArrayTransformer.try_create_dataarray(info)
86+
87+
def layered_array(self, items: list[Any]) -> list[dict]:
88+
netcdf = items[0]
89+
infos = []
90+
for info in items[2:]:
91+
if info is None:
92+
continue
93+
if netcdf:
94+
info["netcdf"] = netcdf
95+
infos.append(ArrayTransformer.try_create_dataarray(info))
96+
return infos
97+
98+
def array(self, items: list[Any]) -> dict[str, Any]:
99+
control = items[0]
100+
data = items[1] if len(items) > 1 else None
101+
if (value := control.get("value", None)) is not None:
102+
data = value
103+
return {"control": control, "data": data}
104+
105+
def control(self, items: list[Any]) -> dict[str, Any]:
106+
return items[0]
107+
108+
def constant(self, items: list[Any]) -> dict[str, Any]:
109+
return {"type": "constant", "value": items[0]}
110+
111+
def internal(self, items: list[Any]) -> dict[str, Any]:
112+
result = {"type": "internal"}
113+
for item in items:
114+
if item is not None:
115+
result.update(item)
116+
return result
117+
118+
def external(self, items: list[Any]) -> dict[str, Any]:
119+
return {"type": "external", "value": items[0]}
120+
121+
def factor(self, items: list[Any]) -> dict[str, float]:
122+
return {"factor": items[0]}
123+
124+
def iprn(self, items: list[Any]) -> dict[str, int]:
125+
return {"iprn": items[0]}
126+
127+
def binary(self, items: list[Any]) -> dict[str, bool]:
128+
return {"binary": True}
129+
130+
def filename(self, items: list[Any]) -> Path:
131+
return Path(items[0])
132+
133+
def data(self, items: list[Any]) -> np.ndarray:
134+
return np.array(items)
135+
136+
def netcdf(self, items: list[Any]) -> dict[str, bool]:
137+
return {"netcdf": True}
138+
139+
def NUMBER(self, token: Token) -> int | float:
140+
return float(token)
141+
142+
def SIGNED_NUMBER(self, token: Token) -> int | float:
143+
return self.NUMBER(token)
144+
145+
def INT(self, token: Token) -> int:
146+
return int(token)
147+
148+
def SIGNED_INT(self, token: Token) -> int:
149+
return int(token)
150+
151+
def ESCAPED_STRING(self, token: Token) -> str:
152+
# Remove quotes from escaped string
153+
value = str(token)
154+
if value.startswith('"') and value.endswith('"'):
155+
return value[1:-1]
156+
return value
157+
158+
@staticmethod
159+
def try_create_dataarray(array_info: dict) -> dict:
160+
"""Create an xarray DataArray from MF6 array information."""
161+
control = array_info["control"]
162+
match control["type"]:
163+
case "constant":
164+
data = control["value"]
165+
array_info["data"] = xr.DataArray(data=data)
166+
case "internal":
167+
data = array_info["data"]
168+
factor = control.get("factor", 1.0)
169+
data = data * factor
170+
array_info["data"] = xr.DataArray(data=data)
171+
case "external":
172+
pass
173+
return array_info

0 commit comments

Comments
 (0)