Skip to content

Commit 5d1c290

Browse files
committed
cleanup
1 parent 26f5f27 commit 5d1c290

File tree

7 files changed

+88
-163
lines changed

7 files changed

+88
-163
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ def get_recarray_name(block_name: str) -> str:
7777
return f"{block_name}data"
7878

7979

80-
def get_recarray_columns(field_names: list[str], block_fields: Mapping[str, FieldV2]) -> list[tuple[str, bool]]:
80+
def get_recarray_columns(
81+
field_names: list[str], block_fields: Mapping[str, FieldV2]
82+
) -> list[tuple[str, bool]]:
8183
"""
8284
Get column names for a recarray with optionality info.
8385
@@ -102,7 +104,7 @@ def get_recarray_columns(field_names: list[str], block_fields: Mapping[str, Fiel
102104
# Add the field names as columns with their optionality
103105
for name in field_names:
104106
field = block_fields[name]
105-
is_optional = getattr(field, 'optional', False)
107+
is_optional = getattr(field, "optional", False)
106108
columns.append((name, is_optional))
107109

108110
return columns
Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Auto-generated grammar for MF6 GWF-WEL
22
%import common.WS
33
%import common.SH_COMMENT
4+
%import common.NEWLINE
45

56
%ignore WS
67
%ignore SH_COMMENT
@@ -16,52 +17,24 @@ dimensions_fields: (maxbound)*
1617
period_fields: (stress_period_data)*
1718
auxiliary: "auxiliary"i array
1819
auxmultname: "auxmultname"i string
19-
boundnames: "boundnames"i
20-
print_input: "print_input"i
21-
print_flows: "print_flows"i
22-
save_flows: "save_flows"i
20+
boundnames: "boundnames"i
21+
print_input: "print_input"i
22+
print_flows: "print_flows"i
23+
save_flows: "save_flows"i
2324
auto_flow_reduce: "auto_flow_reduce"i double
2425
afrcsv_filerecord: "auto_flow_reduce_csv"i "fileout"i string
2526
ts_filerecord: "ts6"i "filein"i string
2627
obs_filerecord: "filein"i "obs6"i string
27-
mover: "mover"i
28+
mover: "mover"i
2829
maxbound: "maxbound"i integer
29-
stress_period_data: (number | simple_string)+
30+
stress_period_data: (number | simple_string)+ NEWLINE
3031

31-
// Inline typed grammar rules
32-
// Named fields use specific types (integer/double) for validation
33-
// List/recarray data uses generic 'number' - structuring step handles type conversion
34-
integer: SIGNED_INT | INT
35-
double: SIGNED_NUMBER | NUMBER
36-
number: SIGNED_NUMBER | NUMBER
37-
string: ESCAPED_STRING | record
38-
simple_string: ESCAPED_STRING | _word
39-
record.1: _token+ _NL
40-
list: record*
41-
array: (single_array | layered_array)
42-
single_array: [netcdf] readarray
43-
layered_array: layered [netcdf] readarray+
44-
layered: "layered"i
45-
netcdf: "netcdf"i
46-
readarray: control [data]
47-
control: constant | internal | external
48-
constant: "constant"i double
49-
internal: "internal"i [factor] [iprn]
50-
external: "open/close"i filename [factor] [binary] [iprn]
51-
factor: "factor"i double
52-
iprn: "iprn"i integer
53-
binary: "(binary)"i
54-
filename: ESCAPED_STRING | _word
55-
data: double+
56-
57-
_word: /[a-zA-Z0-9._'~,-\\(\\)]+/
58-
_token: _word | number
59-
60-
%import common.NEWLINE -> _NL
61-
%import common.CNAME
62-
%import common.WORD
63-
%import common.ESCAPED_STRING
64-
%import common.NUMBER
65-
%import common.INT
66-
%import common.SIGNED_NUMBER
67-
%import common.SIGNED_INT
32+
// Import typed grammar rules
33+
%import typed.integer -> integer
34+
%import typed.double -> double
35+
%import typed.number -> number
36+
%import typed.string -> string
37+
%import typed.simple_string -> simple_string
38+
%import typed.record -> record
39+
%import typed.list -> list
40+
%import typed.array -> array

flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Auto-generated grammar for MF6 {{ name|upper }}
22
%import common.WS
33
%import common.SH_COMMENT
4+
%import common.NEWLINE
45

56
%ignore WS
67
%ignore SH_COMMENT
@@ -82,44 +83,16 @@ opt.type }}{% endif %}
8283
{% set recarray_name = block_name|get_recarray_name %}
8384
{% set field_names = period_groups.values()|first %}
8485
{% set columns = field_names|get_recarray_columns(block_) %}
85-
{{ recarray_name}}: (number | simple_string)+
86+
{{ recarray_name}}: (number | simple_string)+ NEWLINE
8687
{% endif %}
8788
{% endfor %}
8889

89-
// Inline typed grammar rules
90-
// Named fields use specific types (integer/double) for validation
91-
// List/recarray data uses generic 'number' - structuring step handles type conversion
92-
integer: SIGNED_INT | INT
93-
double: SIGNED_NUMBER | NUMBER
94-
number: SIGNED_NUMBER | NUMBER
95-
string: ESCAPED_STRING | record
96-
simple_string: ESCAPED_STRING | _word
97-
record.1: _token+ _NL
98-
list: record*
99-
array: (single_array | layered_array)
100-
single_array: [netcdf] readarray
101-
layered_array: layered [netcdf] readarray+
102-
layered: "layered"i
103-
netcdf: "netcdf"i
104-
readarray: control [data]
105-
control: constant | internal | external
106-
constant: "constant"i double
107-
internal: "internal"i [factor] [iprn]
108-
external: "open/close"i filename [factor] [binary] [iprn]
109-
factor: "factor"i double
110-
iprn: "iprn"i integer
111-
binary: "(binary)"i
112-
filename: ESCAPED_STRING | _word
113-
data: double+
114-
115-
_word: /[a-zA-Z0-9._'~,-\\(\\)]+/
116-
_token: _word | number
117-
118-
%import common.NEWLINE -> _NL
119-
%import common.CNAME
120-
%import common.WORD
121-
%import common.ESCAPED_STRING
122-
%import common.NUMBER
123-
%import common.INT
124-
%import common.SIGNED_NUMBER
125-
%import common.SIGNED_INT
90+
// Import typed grammar rules
91+
%import typed.integer -> integer
92+
%import typed.double -> double
93+
%import typed.number -> number
94+
%import typed.string -> string
95+
%import typed.simple_string -> simple_string
96+
%import typed.record -> record
97+
%import typed.list -> list
98+
%import typed.array -> array

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
integer: _integer
2-
double: _number
1+
// Typed grammar rules for MF6 input files
2+
// Named fields use specific types (integer/double) for validation
3+
// List/recarray data uses generic 'number' - structuring step handles type conversion
4+
integer: SIGNED_INT | INT
5+
double: SIGNED_NUMBER | NUMBER
6+
number: SIGNED_NUMBER | NUMBER
37
string: ESCAPED_STRING | record
4-
record: _token+ _NL
8+
simple_string: ESCAPED_STRING | _word
9+
record.1: _token+ NEWLINE
510
list: record*
611
array: (single_array | layered_array)
712
single_array: [netcdf] readarray
@@ -19,12 +24,10 @@ binary: "(binary)"i
1924
filename: ESCAPED_STRING | _word
2025
data: double+
2126

22-
_word: /[a-zA-Z0-9._'~,-\\(\\)]+/
23-
_number: SIGNED_NUMBER | NUMBER
24-
_integer: SIGNED_INT | INT
25-
_token: _word | _number
27+
_word: /(?!(?i:begin|end))[a-zA-Z0-9._'~,-\\(\\)]+/
28+
_token: _word | number
2629

27-
%import common.NEWLINE -> _NL
30+
%import common.NEWLINE
2831
%import common.WS
2932
%import common.WS_INLINE
3033
%import common.CNAME

flopy4/mf6/codec/reader/transformer.py

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
from collections import ChainMap
2-
from collections.abc import Mapping
31
from pathlib import Path
42
from typing import Any
53

64
import numpy as np
75
import xarray as xr
86
from lark import Token, Transformer
97
from modflow_devtools.dfn import Dfn
10-
from modflow_devtools.dfn.schema.v2 import SCALAR_TYPES
118

129

1310
class BasicTransformer(Transformer):
@@ -76,22 +73,19 @@ def start(self, items: list[Any]) -> dict:
7673
if not isinstance(item, dict):
7774
continue
7875
for block_name, block_data in item.items():
79-
# Pluralize indexed blocks (e.g., period -> periods)
80-
if isinstance(block_data, dict) and all(isinstance(k, int) for k in block_data.keys()):
81-
# This is an indexed block, use plural form
82-
if not block_name.endswith('s'):
83-
block_name = block_name + 's'
84-
85-
if block_name not in merged:
76+
# Check if this is an indexed block (dict with integer keys)
77+
if isinstance(block_data, dict) and all(
78+
isinstance(k, int) for k in block_data.keys()
79+
):
80+
# Flatten indexed blocks into separate keys like "period 1", "period 2"
81+
for index, data in block_data.items():
82+
indexed_key = f"{block_name} {index}"
83+
merged[indexed_key] = data
84+
elif block_name not in merged:
8685
merged[block_name] = block_data
8786
else:
88-
# If both are dicts with integer keys (indexed blocks), merge them
89-
existing = merged[block_name]
90-
if (isinstance(existing, dict) and isinstance(block_data, dict) and
91-
all(isinstance(k, int) for k in existing.keys()) and
92-
all(isinstance(k, int) for k in block_data.keys())):
93-
existing.update(block_data)
94-
# Otherwise, indexed block overwrites (shouldn't happen for well-formed input)
87+
# This shouldn't happen for well-formed input
88+
pass
9589
return merged
9690

9791
def block(self, items: list[Any]) -> dict:
@@ -160,7 +154,7 @@ def factor(self, items: list[Any]) -> dict[str, float]:
160154
def iprn(self, items: list[Any]) -> dict[str, int]:
161155
return {"iprn": items[0]}
162156

163-
def binary(self, items: list[Any]) -> dict[str, bool]:
157+
def binary(self, _) -> dict[str, bool]:
164158
return {"binary": True}
165159

166160
def filename(self, items: list[Any]) -> Path:
@@ -193,59 +187,20 @@ def number(self, items: list[Any]) -> int | float:
193187
def data(self, items: list[Any]) -> np.ndarray:
194188
return np.array(items)
195189

196-
def netcdf(self, items: list[Any]) -> dict[str, bool]:
190+
def netcdf(self, _) -> dict[str, bool]:
197191
return {"netcdf": True}
198192

199-
# Handle typed__ prefixed rules from imports
200-
def typed__single_array(self, items: list[Any]) -> dict:
201-
return self.single_array(items)
202-
203-
def typed__layered_array(self, items: list[Any]) -> list[dict]:
204-
return self.layered_array(items)
205-
206-
def typed__readarray(self, items: list[Any]) -> dict[str, Any]:
207-
return self.readarray(items)
208-
209-
def typed__control(self, items: list[Any]) -> dict[str, Any]:
210-
return self.control(items)
211-
212-
def typed__constant(self, items: list[Any]) -> dict[str, Any]:
213-
return self.constant(items)
214-
215-
def typed__internal(self, items: list[Any]) -> dict[str, Any]:
216-
return self.internal(items)
217-
218-
def typed__external(self, items: list[Any]) -> dict[str, Any]:
219-
return self.external(items)
220-
221-
def typed__factor(self, items: list[Any]) -> dict[str, float]:
222-
return self.factor(items)
223-
224-
def typed__iprn(self, items: list[Any]) -> dict[str, int]:
225-
return self.iprn(items)
226-
227-
def typed__binary(self, items: list[Any]) -> dict[str, bool]:
228-
return self.binary(items)
229-
230-
def typed__filename(self, items: list[Any]) -> Path:
231-
return self.filename(items)
232-
233-
def typed__data(self, items: list[Any]) -> np.ndarray:
234-
return self.data(items)
235-
236-
def typed__netcdf(self, items: list[Any]) -> dict[str, bool]:
237-
return self.netcdf(items)
238-
239-
def typed__layered(self, items: list[Any]) -> dict[str, bool]:
240-
return {"layered": True}
241-
242193
def block_index(self, items: list[Any]) -> int:
243194
"""Extract block index (e.g., period number)."""
244195
return items[0]
245196

246197
def stress_period_data(self, items: list[Any]) -> list[Any]:
247-
"""Handle stress period data (list of values for a single record)."""
248-
return items
198+
"""Handle stress period data (one row of values).
199+
200+
The parser gives us the values for one row plus a NEWLINE token.
201+
Filter out the NEWLINE token and return just the data values.
202+
"""
203+
return [item for item in items if not isinstance(item, Token) or item.type != "NEWLINE"]
249204

250205
@staticmethod
251206
def try_create_dataarray(array_info: dict) -> dict:
@@ -263,7 +218,7 @@ def __default__(self, data, children, meta):
263218
if self.blocks is None or self.fields is None:
264219
return super().__default__(data, children, meta)
265220
if data.endswith("_block") and (block_name := data[:-6]) in self.blocks:
266-
# Check if this is an indexed block (period blocks have 3 children: [index, fields, index])
221+
# See if this is an indexed block (period blocks have 3 children: index, fields, index
267222
if len(children) == 3 and isinstance(children[0], int) and isinstance(children[2], int):
268223
# Indexed block: [index, fields, index]
269224
block_index = children[0]

test/test_mf6_grammar_gen.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ def test_make_grammar_creates_file(tmp_path, minimal_dfn):
5959
assert expected_file.is_file()
6060
content = expected_file.read_text()
6161
assert "// Auto-generated grammar for MF6 TEST-COMPONENT" in content
62-
assert '%import "typed.lark"' in content
62+
# Grammar imports typed rules from typed.lark
63+
assert "%import typed.integer -> integer" in content
64+
assert "%import typed.double -> double" in content
6365
assert "start: block*" in content
6466
assert "options_block" in content
6567

@@ -182,7 +184,11 @@ def test_make_grammar_with_period_block(tmp_path):
182184
lines = content.split("\n")
183185
period_fields_line = [l for l in lines if "period_fields:" in l][0]
184186
assert "stress_period_data" in period_fields_line
185-
assert "cellid" in content.lower()
187+
188+
# stress_period_data should accept numbers and strings, one row per line
189+
assert "stress_period_data:" in content
190+
stress_period_data_line = [l for l in lines if l.strip().startswith("stress_period_data:")][0]
191+
assert "NEWLINE" in stress_period_data_line
186192

187193

188194
def test_make_grammar_with_named_subfields(tmp_path):
@@ -206,6 +212,10 @@ def test_make_grammar_with_named_subfields(tmp_path):
206212
grammar_file = tmp_path / "gwf-rch.lark"
207213
content = grammar_file.read_text()
208214

209-
assert "stress_period_data: cellid recharge" in content
210-
assert "cellid: integer+" in content
211-
assert "recharge: double" in content
215+
# stress_period_data should be a generic recarray accepting numbers/strings per line
216+
assert "stress_period_data" in content
217+
lines = content.split("\n")
218+
stress_period_data_line = [l for l in lines if l.strip().startswith("stress_period_data:")][0]
219+
assert "NEWLINE" in stress_period_data_line
220+
# Should accept both numbers and simple strings
221+
assert "number" in stress_period_data_line or "simple_string" in stress_period_data_line

test/test_mf6_reader.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,5 +344,14 @@ def test_transform_gwf_wel_file(model_workspace):
344344

345345
# Check structure
346346
assert isinstance(result, dict)
347-
assert "periods" in result # WEL has periods
348-
assert len(result["periods"]) > 0 # Should have at least one period
347+
348+
# Should have a period 2 entry (indexed period blocks are flattened to "period N" keys)
349+
assert "period 2" in result
350+
assert "stress_period_data" in result["period 2"]
351+
352+
# Should have 2 rows of data (MAXBOUND = 2)
353+
assert len(result["period 2"]["stress_period_data"]) == 2
354+
355+
# Each row should have 4 values (cellid components + q value)
356+
assert len(result["period 2"]["stress_period_data"][0]) == 4
357+
assert len(result["period 2"]["stress_period_data"][1]) == 4

0 commit comments

Comments
 (0)