Skip to content

Commit f102362

Browse files
author
wpbonelli
committed
handle scalars, test component-specific parsing/transformation
1 parent ed2c3af commit f102362

File tree

5 files changed

+160
-53
lines changed

5 files changed

+160
-53
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
start: [WS] [_NL*] (block [[WS] _NL*])+ [WS]
2-
block: "begin"i block_name _NL _content "end"i block_name _NL+
2+
block: "begin"i block_name _NL _list "end"i block_name _NL+
33
block_name: CNAME [INT]
4-
_content: line*
4+
_list: line*
55
line: [WS] item+ _NL+
66
item: word | NUMBER
77
word: /[a-zA-Z0-9._'~,-\\(\\)]+/

flopy4/mf6/codec/reader/grammar/array.lark renamed to flopy4/mf6/codec/reader/grammar/typed.lark

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1-
start: readarray
2-
readarray: (single_array | layered_array)
3-
single_array: [netcdf] array
4-
layered_array: layered [netcdf] array+
1+
integer: _integer
2+
double: _number
3+
string: ESCAPED_STRING | record
4+
record: _token+ _NL
5+
list: record*
6+
array: (single_array | layered_array)
7+
single_array: [netcdf] readarray
8+
layered_array: layered [netcdf] readarray+
59
layered: "layered"i
610
netcdf: "netcdf"i
7-
array: control [data]
11+
readarray: control [data]
812
control: constant | internal | external
913
constant: "constant"i _number
1014
internal: "internal"i [factor] [iprn]
1115
external: "open/close"i filename [factor] [binary] [iprn]
1216
factor: "factor"i _number
13-
iprn: "iprn"i _signed_integer
14-
binary: "(" "binary"i ")"
17+
iprn: "iprn"i _integer
18+
binary: "(binary)"i
1519
filename: ESCAPED_STRING | _word
1620
data: _number+
17-
_number: SIGNED_NUMBER | NUMBER
18-
_signed_integer: SIGNED_INT | INT
21+
1922
_word: /[a-zA-Z0-9._'~,-\\(\\)]+/
23+
_number: SIGNED_NUMBER | NUMBER
24+
_integer: SIGNED_INT | INT
25+
_token: _word | _number
2026

2127
%import common.NEWLINE -> _NL
2228
%import common.WS

flopy4/mf6/codec/reader/parser.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,3 @@ def make_basic_parser() -> Lark:
88
with open(grammar_path, "r") as f:
99
grammar = f.read()
1010
return Lark(grammar, parser="lalr", debug=True)
11-
12-
13-
def make_array_parser() -> Lark:
14-
grammar_path = Path(__file__).parent / "grammar" / "array.lark"
15-
with open(grammar_path, "r") as f:
16-
grammar = f.read()
17-
return Lark(grammar, parser="lalr", debug=True)

flopy4/mf6/codec/reader/transformer.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ def block(self, items: list[Any]) -> dict[str, Any]:
2727
def block_name(self, items: list[Any]) -> str:
2828
return " ".join([str(item) for item in items if item is not None])
2929

30+
def _list(self, items: list[Any]) -> list[Any]:
31+
return items[0] if items else []
32+
3033
def line(self, items: list[Any]) -> list[Any]:
3134
return items[1:]
3235

@@ -53,19 +56,13 @@ def INT(self, token: Token) -> int:
5356
return int(token)
5457

5558

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-
"""
59+
class TypedTransformer(Transformer):
60+
"""Type-aware transformer for MF6 input files."""
6461

6562
def start(self, items: list[Any]) -> dict:
6663
return items[0]
6764

68-
def readarray(self, items: list[Any]) -> dict:
65+
def array(self, items: list[Any]) -> dict:
6966
infos = items[0]
7067
if isinstance(infos, list):
7168
data = xr.concat([info["data"] for info in infos if "data" in info], dim="layer")
@@ -82,7 +79,7 @@ def single_array(self, items: list[Any]) -> dict:
8279
info = items[-1]
8380
if netcdf:
8481
info["netcdf"] = netcdf
85-
return ArrayTransformer.try_create_dataarray(info)
82+
return TypedTransformer.try_create_dataarray(info)
8683

8784
def layered_array(self, items: list[Any]) -> list[dict]:
8885
netcdf = items[0]
@@ -92,10 +89,10 @@ def layered_array(self, items: list[Any]) -> list[dict]:
9289
continue
9390
if netcdf:
9491
info["netcdf"] = netcdf
95-
infos.append(ArrayTransformer.try_create_dataarray(info))
92+
infos.append(TypedTransformer.try_create_dataarray(info))
9693
return infos
9794

98-
def array(self, items: list[Any]) -> dict[str, Any]:
95+
def readarray(self, items: list[Any]) -> dict[str, Any]:
9996
control = items[0]
10097
data = items[1] if len(items) > 1 else None
10198
if (value := control.get("value", None)) is not None:
@@ -116,7 +113,11 @@ def internal(self, items: list[Any]) -> dict[str, Any]:
116113
return result
117114

118115
def external(self, items: list[Any]) -> dict[str, Any]:
119-
return {"type": "external", "value": items[0]}
116+
result = {"type": "external", "value": items[0]}
117+
for item in items[1:]:
118+
if item is not None:
119+
result.update(item)
120+
return result
120121

121122
def factor(self, items: list[Any]) -> dict[str, float]:
122123
return {"factor": items[0]}
@@ -130,6 +131,15 @@ def binary(self, items: list[Any]) -> dict[str, bool]:
130131
def filename(self, items: list[Any]) -> Path:
131132
return Path(items[0])
132133

134+
def string(self, items: list[Any]) -> str:
135+
return items[0].strip("\"'")
136+
137+
def integer(self, items: list[Any]) -> int:
138+
return int(items[0])
139+
140+
def double(self, items: list[Any]) -> float:
141+
return float(items[0])
142+
133143
def data(self, items: list[Any]) -> np.ndarray:
134144
return np.array(items)
135145

@@ -157,17 +167,12 @@ def ESCAPED_STRING(self, token: Token) -> str:
157167

158168
@staticmethod
159169
def try_create_dataarray(array_info: dict) -> dict:
160-
"""Create an xarray DataArray from MF6 array information."""
161170
control = array_info["control"]
162171
match control["type"]:
163172
case "constant":
164-
data = control["value"]
165-
array_info["data"] = xr.DataArray(data=data)
173+
array_info["data"] = xr.DataArray(data=control["value"])
166174
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)
175+
array_info["data"] = xr.DataArray(data=array_info["data"])
171176
case "external":
172177
pass
173178
return array_info

test/test_parse_array.py

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1+
import os
2+
from collections import ChainMap
13
from pathlib import Path
4+
from typing import Any
25

36
import numpy as np
47
import xarray as xr
8+
from lark import Lark
59

6-
from flopy4.mf6.codec.reader.parser import make_array_parser
7-
from flopy4.mf6.codec.reader.transformer import ArrayTransformer
10+
from flopy4.mf6.codec.reader.transformer import TypedTransformer
11+
12+
PROJ_ROOT_PATH = Path(__file__).parents[1]
13+
BASE_GRAMMAR_PATH = (
14+
PROJ_ROOT_PATH / "flopy4" / "mf6" / "codec" / "reader" / "grammar" / "typed.lark"
15+
)
16+
17+
18+
def make_typed_parser(grammar: str):
19+
with open(BASE_GRAMMAR_PATH, "r") as f:
20+
return Lark(grammar + os.linesep + f.read(), parser="lalr", debug=True)
821

922

1023
def test_parse_internal_array():
11-
parser = make_array_parser()
24+
parser = make_typed_parser("start: array")
1225
tree = parser.parse("""
1326
INTERNAL FACTOR 1.0 IPRN 3
1427
1.2 3.7 9.3 4.2 2.2 9.9 1.0
@@ -20,7 +33,7 @@ def test_parse_internal_array():
2033

2134

2235
def test_parse_layered_array():
23-
parser = make_array_parser()
36+
parser = make_typed_parser("start: array")
2437
tree = parser.parse("""
2538
LAYERED
2639
CONSTANT 1.0
@@ -34,32 +47,32 @@ def test_parse_layered_array():
3447

3548

3649
def test_parse_constant_array():
37-
parser = make_array_parser()
50+
parser = make_typed_parser("start: array")
3851
tree = parser.parse("""
3952
CONSTANT 1.0
4053
""")
4154
print(tree.pretty())
4255

4356

4457
def test_parse_external_array_no_quotation_marks():
45-
parser = make_array_parser()
58+
parser = make_typed_parser("start: array")
4659
tree = parser.parse("""
4760
OPEN/CLOSE some.file
4861
""")
4962
print(tree.pretty())
5063

5164

5265
def test_parse_external_array_with_quotation_marks():
53-
parser = make_array_parser()
66+
parser = make_typed_parser("start: array")
5467
tree = parser.parse("""
5568
OPEN/CLOSE "some.file"
5669
""")
5770
print(tree.pretty())
5871

5972

6073
def test_transform_internal_array():
61-
parser = make_array_parser()
62-
transformer = ArrayTransformer()
74+
parser = make_typed_parser("start: array")
75+
transformer = TypedTransformer()
6376
result = transformer.transform(
6477
parser.parse("""
6578
INTERNAL FACTOR 1.5 IPRN 3
@@ -76,8 +89,8 @@ def test_transform_internal_array():
7689

7790

7891
def test_transform_constant_array():
79-
parser = make_array_parser()
80-
transformer = ArrayTransformer()
92+
parser = make_typed_parser("start: array")
93+
transformer = TypedTransformer()
8194
result = transformer.transform(
8295
parser.parse("""
8396
CONSTANT 42.5
@@ -88,8 +101,8 @@ def test_transform_constant_array():
88101

89102

90103
def test_transform_external_array():
91-
parser = make_array_parser()
92-
transformer = ArrayTransformer()
104+
parser = make_typed_parser("start: array")
105+
transformer = TypedTransformer()
93106
result = transformer.transform(
94107
parser.parse("""
95108
OPEN/CLOSE "data/heads.dat" FACTOR 1.0 (BINARY)
@@ -100,8 +113,8 @@ def test_transform_external_array():
100113

101114

102115
def test_transform_layered_array():
103-
parser = make_array_parser()
104-
transformer = ArrayTransformer()
116+
parser = make_typed_parser("start: array")
117+
transformer = TypedTransformer()
105118
result = transformer.transform(
106119
parser.parse("""
107120
LAYERED
@@ -119,3 +132,93 @@ def test_transform_layered_array():
119132
assert result["data"].shape == (2, 8)
120133
assert result["data"].dims == ("layer", "dim_0")
121134
assert np.array_equal(result["data"][0], np.ones((8,)))
135+
136+
137+
def test_transform_full_component():
138+
grammar = """
139+
start: block*
140+
block: options_block | arrays_block
141+
options_block: "begin"i "options"i options_vars "end"i "options"i
142+
options_vars: (r2d2 | b | c | p)*
143+
arrays_block: "begin"i "arrays"i arrays_vars "end"i "arrays"i
144+
arrays_vars: (x | y | z)*
145+
r2d2: "r2d2"i // keyword
146+
b: "b"i string
147+
c: "c"i integer
148+
p: "p"i double
149+
x: "x"i array
150+
y: "y"i array
151+
z: "z"i array
152+
"""
153+
parser = make_typed_parser(grammar)
154+
155+
class BlockTransformer(TypedTransformer):
156+
def start(self, items: list[Any]) -> dict:
157+
return ChainMap(*items)
158+
159+
def block(self, items: list[Any]) -> dict:
160+
return items[0]
161+
162+
def options_block(self, items: list[Any]) -> dict:
163+
return {"options": items[0]}
164+
165+
def arrays_block(self, items: list[Any]) -> dict:
166+
return {"arrays": items[0]}
167+
168+
def options_vars(self, items: list[Any]) -> dict:
169+
return {item[0].lower(): item[1] for item in items}
170+
171+
def arrays_vars(self, items: list[Any]) -> dict:
172+
return {item[0].lower(): item[1] for item in items}
173+
174+
def r2d2(self, _: list[Any]) -> bool:
175+
return "r2d2", True
176+
177+
def b(self, items: list[Any]) -> tuple[str, str]:
178+
return "b", items[0]
179+
180+
def c(self, items: list[Any]) -> tuple[str, int]:
181+
return "c", items[0]
182+
183+
def p(self, items: list[Any]) -> tuple[str, float]:
184+
return "p", items[0]
185+
186+
def x(self, items: list[Any]) -> tuple[str, dict]:
187+
return "x", TypedTransformer.try_create_dataarray(items[0])
188+
189+
def y(self, items: list[Any]) -> tuple[str, dict]:
190+
return "y", TypedTransformer.try_create_dataarray(items[0])
191+
192+
def z(self, items: list[Any]) -> tuple[str, dict]:
193+
return "z", TypedTransformer.try_create_dataarray(items[0])
194+
195+
transformer = BlockTransformer()
196+
result = transformer.transform(
197+
parser.parse("""
198+
BEGIN OPTIONS
199+
R2D2
200+
B "nice said"
201+
C 3
202+
P 0.
203+
END OPTIONS
204+
BEGIN ARRAYS
205+
X CONSTANT 1.0
206+
Y INTERNAL 4.0 5.0 6.0
207+
Z OPEN/CLOSE "data/z.dat" FACTOR 1.0 (BINARY)
208+
END ARRAYS
209+
""")
210+
)
211+
assert "options" in result
212+
assert "arrays" in result
213+
assert result["options"]["r2d2"] is True
214+
assert result["options"]["b"] == "nice said"
215+
assert result["options"]["c"] == 3
216+
assert result["options"]["p"] == 0.0
217+
assert result["arrays"]["x"]["control"]["type"] == "constant"
218+
assert np.array_equal(result["arrays"]["x"]["data"], np.array(1.0))
219+
assert result["arrays"]["y"]["control"]["type"] == "internal"
220+
assert np.array_equal(result["arrays"]["y"]["data"], np.array([4.0, 5.0, 6.0]))
221+
assert result["arrays"]["z"]["control"]["type"] == "external"
222+
assert result["arrays"]["z"]["control"]["factor"] == 1.0
223+
assert result["arrays"]["z"]["control"]["binary"] is True
224+
assert result["arrays"]["z"]["data"] == Path("data/z.dat")

0 commit comments

Comments
 (0)