Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions flopy4/mf6/codec/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
from typing import Any

import xarray as xr
from modflow_devtools.dfns.schema.field import Field
from modflow_devtools.dfns.schema.v2 import FieldType
from modflow_devtools.dfn.schema import FieldType


def field_type(value: Any) -> FieldType:
"""Get a value's type according to the MF6 specification."""

if isinstance(value, Field):
return value.type
if isinstance(value, bool):
return "keyword"
if isinstance(value, int):
Expand All @@ -20,7 +17,11 @@ def field_type(value: Any) -> FieldType:
return "double"
if isinstance(value, str):
return "string"
if isinstance(value, (dict, tuple)):
if isinstance(value, tuple):
return "record"
if isinstance(value, dict):
if type_ := value.get("type", None):
return type_
return "record"
if isinstance(value, xr.DataArray):
if value.dtype == "object":
Expand Down
6 changes: 3 additions & 3 deletions flopy4/mf6/codec/reader/grammar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ def make_grammar(dfn: Dfn, outdir: PathLike):
outdir = Path(outdir).expanduser().resolve().absolute()
env = _get_env()
template = env.get_template("component.lark.jinja")
target_path = outdir / f"{dfn.name}.lark"
blocks, fields = _get_template_data(dfn.blocks)
target_path = outdir / f"{dfn['name']}.lark"
blocks, fields = _get_template_data(dfn["blocks"])
with open(target_path, "w") as f:
name = dfn.name
name = dfn["name"]
f.write(template.render(name=name, blocks=blocks, fields=fields))


Expand Down
30 changes: 15 additions & 15 deletions flopy4/mf6/codec/reader/grammar/filters.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
from collections.abc import Mapping

from modflow_devtools.dfns.schema.v2 import FieldV2
from modflow_devtools.dfn.schema import Field


def field_type(field: FieldV2) -> str:
match field.type:
case t if t in ["string", "integer", "double"] and field.shape:
if "period" in field.block:
def field_type(field: Field) -> str:
match field["type"]:
case t if t in ["string", "integer", "double"] and field.get("shape", None):
if "period" in field["block"]:
return "list"
return "array"
case "keyword":
return ""
case "union":
return "" # keystrings generate their own union rules
case _:
return field.type
return field["type"]


def record_child_type(field: FieldV2) -> str:
def record_child_type(field: Field) -> str:
"""
Get the grammar type for a field within a record context.

In records, string fields should use 'word' instead of 'string'
to avoid consuming the rest of the line (since string matches token+ NEWLINE).
"""
match field.type:
match field["type"]:
case "string":
return "word" # Use word for strings in records to match single tokens
case t if t in ["double", "integer"]:
Expand All @@ -34,21 +34,21 @@ def record_child_type(field: FieldV2) -> str:
case "union":
return "" # unions generate their own union rules
case _:
return field.type
return field["type"]


def is_period_list_field(field: FieldV2) -> bool:
def is_period_list_field(field: Field) -> bool:
"""Check if a field is part of a period block list/recarray."""
if not field.shape or not field.block:
if not field.get("shape", None) or not field.get("block", None):
return False
return (
"period" in field.block
and field.type in ["string", "integer", "double"]
and field.shape is not None
"period" in field["block"]
and field["type"] in ["string", "integer", "double"]
and field["shape"] is not None
)


def group_period_fields(block_fields: Mapping[str, FieldV2]) -> dict[str, list[str]]:
def group_period_fields(block_fields: Mapping[str, Field]) -> dict[str, list[str]]:
"""
Group period block fields that should be combined into a single list.

Expand Down
40 changes: 20 additions & 20 deletions flopy4/mf6/codec/reader/transformer/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
import xarray as xr
from lark import Token, Transformer
from modflow_devtools.dfn import Dfn
from modflow_devtools.dfn import Dfn, get_fields

from flopy4.utils import parse_number

Expand All @@ -15,8 +15,8 @@ class TypedTransformer(Transformer):
def __init__(self, visit_tokens=False, dfn: Dfn = None):
super().__init__(visit_tokens)
self.dfn = dfn
self.blocks = dfn.blocks if dfn else None
self.fields = dfn.fields if dfn else None
self.blocks = dfn["blocks"] if dfn else None
self.fields = get_fields(dfn) if dfn else None
# Create a flattened fields dict that includes nested fields
self._flat_fields = self._flatten_fields(self.fields) if self.fields else None

Expand All @@ -32,13 +32,13 @@ def _flatten_fields(self, fields: dict) -> dict:
"""Recursively flatten fields dict to include children of records and unions."""
flat = dict(fields) # Start with top-level fields
for field in fields.values():
if hasattr(field, "children") and field.children:
if "children" in field and field["children"]:
# Add children fields
for child_name, child_field in field.children.items():
for child_name, child_field in field["children"].items():
flat[child_name] = child_field
# Recursively flatten nested children
if hasattr(child_field, "children") and child_field.children:
nested_flat = self._flatten_fields(child_field.children)
if "children" in child_field and child_field["children"]:
nested_flat = self._flatten_fields(child_field["children"])
flat.update(nested_flat)
return flat

Expand Down Expand Up @@ -143,7 +143,7 @@ def string(self, items: list[Any]) -> str:
return value.strip("\"'")
else:
# It's a tree, extract the token value
return str(value.children[0]) if hasattr(value, "children") else str(value)
return str(value["children"][0]) if hasattr(value, "children") else str(value)

def simple_string(self, items: list[Any]) -> str:
"""Handle simple string (unquoted word or escaped string)."""
Expand Down Expand Up @@ -197,7 +197,7 @@ def record(self, items: list[Any]) -> list[Any]:
token_child = item.children[0]
if hasattr(token_child, "children") and len(token_child.children) > 0:
# This is a number tree, get the actual value
values.append(token_child.children[0])
values.append(token_child["children"][0])
else:
# This is a direct value (string)
values.append(token_child)
Expand Down Expand Up @@ -270,14 +270,14 @@ def __default__(self, data, children, meta):
field_name, alternative_name = parts
if (parent_field := self._flat_fields.get(field_name, None)) is not None:
if (
parent_field.type == "union"
and hasattr(parent_field, "children")
and parent_field.children
and alternative_name in parent_field.children
parent_field["type"] == "union"
and "children" in parent_field
and parent_field["children"]
and alternative_name in parent_field["children"]
):
# This is a union alternative
alt_field = parent_field.children[alternative_name]
if alt_field.type == "keyword":
alt_field = parent_field["children"][alternative_name]
if alt_field["type"] == "keyword":
# Keyword alternatives return just the alternative name
return alternative_name
else:
Expand All @@ -289,17 +289,17 @@ def __default__(self, data, children, meta):
# Try with hyphens instead of underscores (reverse of to_rule_name)
field = self._flat_fields.get(data.replace("_", "-"), None)
if field is not None:
if field.type == "keyword":
if field["type"] == "keyword":
return data, True
elif field.type == "record" and hasattr(field, "children") and field.children:
elif field["type"] == "record" and "children" in field and field["children"]:
# Transform record fields into dicts with child field names as keys
# Keyword children are literals in the grammar and don't appear in children list
# Only non-keyword children appear in the children list
record_dict = {}
non_keyword_children = [
(name, child)
for name, child in field.children.items()
if child.type != "keyword"
for name, child in field["children"].items()
if child["type"] != "keyword"
]
for i, (child_name, child_field) in enumerate(non_keyword_children):
if i < len(children):
Expand All @@ -309,7 +309,7 @@ def __default__(self, data, children, meta):
else:
record_dict[child_name] = children[i]
return data, record_dict
elif field.type == "union" and hasattr(field, "children") and field.children:
elif field["type"] == "union" and "children" in field and field["children"]:
# For union fields, return the transformed child
# The parser will have selected one alternative
return data, children[0] if len(children) == 1 else children
Expand Down
3 changes: 1 addition & 2 deletions flopy4/mf6/converter/egress/unstructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
import numpy as np
import xarray as xr
import xattree
from modflow_devtools.dfns.schema.block import block_sort_key
from xattree import XatSpec

from flopy4.mf6.binding import Binding
from flopy4.mf6.component import Component
from flopy4.mf6.constants import FILL_DNODATA
from flopy4.mf6.context import Context
from flopy4.mf6.spec import FileInOut, blocks_dict
from flopy4.mf6.spec import FileInOut, block_sort_key, blocks_dict


def _path_to_tuple(name: str, value: Path, inout: FileInOut) -> tuple[str, ...]:
Expand Down
8 changes: 2 additions & 6 deletions flopy4/mf6/converter/ingress/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,15 +1003,15 @@ def structure_array(
coords,
list(coords_dict.values()),
shape=shape,
fill_value=field.default or FILL_DNODATA,
fill_value=FILL_DNODATA,
)
else:
# Empty dict - return empty sparse array
result = sparse.COO(
np.empty((len(shape), 0), dtype=int),
[],
shape=shape,
fill_value=field.default or FILL_DNODATA,
fill_value=FILL_DNODATA,
)
else:
# Dense approach
Expand Down Expand Up @@ -1086,10 +1086,6 @@ def structure_array(
# For multi-dimensional arrays with object dtype, store the object
result[kper] = val

# Apply fill value replacement (skip for object dtypes)
if field.dtype != np.object_:
result[result == FILL_DNODATA] = field.default or FILL_DNODATA

elif isinstance(value, list):
# List format
result = _parse_list_format(value, dims_names, tuple(shape), field)
Expand Down
6 changes: 3 additions & 3 deletions flopy4/mf6/gwe/est.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ class Est(Package):
block="options", default=False, longname="activate zero-order decay in solid phase"
)
density_water: Optional[float] = field(
block="options", default=None, longname="density of water"
block="options", default="1000.0", longname="density of water"
)
heat_capacity_water: Optional[float] = field(
block="options", default=None, longname="heat capacity of water"
block="options", default="4184.0", longname="heat capacity of water"
)
latent_heat_vaporization: Optional[float] = field(
block="options", default=None, longname="latent heat of vaporization"
block="options", default="2453500.0", longname="latent heat of vaporization"
)
porosity: NDArray[np.float64] = array(
block="griddata",
Expand Down
2 changes: 1 addition & 1 deletion flopy4/mf6/gwe/ic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Ic(Package):
strt: NDArray[np.float64] = array(
block="griddata",
dims=("nodes",),
default=None,
default="0.0",
netcdf=True,
converter=Converter(structure_array, takes_self=True, takes_field=True),
longname="starting temperature",
Expand Down
1 change: 1 addition & 0 deletions flopy4/mf6/gwe/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class Lke(Package):
converter=Converter(structure_array, takes_self=True, takes_field=True),
longname="lake name",
)
# TODO: laksetting — type 'union' not yet supported
status: Optional[NDArray[np.object_]] = embedded_keystring(
"STATUS",
"nlakes",
Expand Down
4 changes: 3 additions & 1 deletion flopy4/mf6/gwf/buy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class Buy(Package):
hhformulation_rhs: bool = field(
block="options", default=False, longname="hh formulation on right-hand side"
)
denseref: Optional[float] = field(block="options", default=None, longname="reference density")
denseref: Optional[float] = field(
block="options", default="1000.", longname="reference density"
)
density_file: Optional[Path] = path(
block="options", default=None, converter=to_path, inout="fileout"
)
Expand Down
6 changes: 3 additions & 3 deletions flopy4/mf6/gwf/chd.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ class Chd(Package):
block="options", default=False, longname="print input to listing file"
)
print_flows: bool = field(
block="options", default=False, longname="print chd flows to listing file"
block="options", default=False, longname="print CHD flows to listing file"
)
save_flows: bool = field(
block="options", default=False, longname="save chd flows to budget file"
block="options", default=False, longname="save CHD flows to budget file"
)
ts_file: Optional[Path] = path(block="options", default=None, converter=to_path, inout="filein")
obs_file: Optional[Path] = path(
block="options", default=None, converter=to_path, inout="filein"
)
dev_no_newton: bool = field(
block="options", default=False, longname="turn off newton for unconfined cells"
block="options", default=False, longname="turn off Newton for unconfined cells"
)
maxbound: Optional[int] = field(
block="dimensions", default=None, init=False, longname="maximum number of constant heads"
Expand Down
8 changes: 4 additions & 4 deletions flopy4/mf6/gwf/chdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ class Chdg(Package):
block="options", default=False, longname="print input to listing file"
)
print_flows: bool = field(
block="options", default=False, longname="print chd flows to listing file"
block="options", default=False, longname="print CHD flows to listing file"
)
save_flows: bool = field(
block="options", default=False, longname="save chd flows to budget file"
block="options", default=False, longname="save CHD flows to budget file"
)
obs_file: Optional[Path] = path(
block="options", default=None, converter=to_path, inout="filein"
Expand All @@ -44,7 +44,7 @@ class Chdg(Package):
block="options", default=False, longname="export array variables to netcdf output files."
)
dev_no_newton: bool = field(
block="options", default=False, longname="turn off newton for unconfined cells"
block="options", default=False, longname="turn off Newton for unconfined cells"
)
maxbound: Optional[int] = field(
block="dimensions",
Expand All @@ -56,7 +56,7 @@ class Chdg(Package):
head: Optional[NDArray[np.float64]] = array(
block="period",
dims=("nper", "nodes"),
default=None,
default="3.e30",
netcdf=True,
converter=Converter(structure_array, takes_self=True, takes_field=True),
on_setattr=update_maxbound,
Expand Down
Loading
Loading