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
24 changes: 20 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,17 @@ jobs:
environments: test${{ matrix.python }}
activate-environment: true

# temporary. devtools 2.x will autosync, but 1.x needs opt-in.
# temporary. devtools 2.x will autosync, but 1.x needs opt-in. A DFN
# sync failure fails the job; a models one doesn't yet, since the
# mf6/example registry is missing its models.toml.
- name: Sync devtools
run: pixi run mf sync
# bash on Windows too: pwsh exits with the models sync's code
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
pixi run mf dfns sync
pixi run mf models sync || echo "::warning::models registry sync failed"

- name: Show MF6 sync status
run: pixi run flopy4 mf6 status
Expand Down Expand Up @@ -132,9 +140,17 @@ jobs:
environments: test313
activate-environment: true

# temporary. devtools 2.x will autosync, but 1.x needs opt-in.
# temporary. devtools 2.x will autosync, but 1.x needs opt-in. A DFN
# sync failure fails the job; a models one doesn't yet, since the
# mf6/example registry is missing its models.toml.
- name: Sync devtools
run: pixi run mf sync
# bash on Windows too: pwsh exits with the models sync's code
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
pixi run mf dfns sync
pixi run mf models sync || echo "::warning::models registry sync failed"

- name: Sync classes and binary to the latest MF6 release
env:
Expand Down
4 changes: 2 additions & 2 deletions docs/dev/sdd.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ A top-level-template `blocks.jinja` iterates over blocks, calling field macros d
- **Scalars**: keywords, integers, floats, strings
- **Records**: tuples of values (e.g., file specifications, cell IDs with values)
- **Arrays**: numeric arrays with control records (`CONSTANT`, `INTERNAL`, `OPEN/CLOSE`)
- **Lists**: stress period data, either tabular or keystring format
- **Keystrings**: option records with keyword-value pairs
- **Lists**: stress period data, rows of records or unions of keyword-led settings
- **Unions**: one of several alternatives, e.g. a keyword-led setting

Custom Jinja filters in `flopy4.mf6.codec.writer.filters` implement field-specific logic.

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
assert chd.stress_period_data[0][1].head == 0.0
assert gwf.dis.to_dataarray("botm").sel(layer=0, y=0, x=0) == 0.0
assert gwf.oc.stress_period_data[0][0].rtype == "HEAD"
assert isinstance(gwf.oc.stress_period_data[0][0].ocsetting, Oc.All)
assert gwf.oc.stress_period_data[0][0].ocsetting == "ALL"

# ### Read results
#
Expand Down
7 changes: 7 additions & 0 deletions flopy4/mf6/codec/reader/grammar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,16 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object]
standalone_fields = []
# a list's element lines, each read into the list (see names.py)
elements = set()
# tagged `valid` scalars, whose name precedes their value. The union
# valid_as_union makes of one is tagged by its keyword arms, so it
# can't say whether the scalar was.
tagged_valid = set()

for field_name, field in block.fields.items():
if field.removed:
continue
if field.tagged and getattr(field, "valid", None):
tagged_valid.add(field_name)
field = filters.valid_as_union(field)
if isinstance(field, List):
if isinstance(field.item, Union):
Expand Down Expand Up @@ -107,6 +113,7 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object]
"header": header.name if isinstance(header, Record) else None,
"standalone_fields": standalone_fields,
"rules": rules,
"tagged_valid": tagged_valid,
"recarrays": recarrays,
"redirect": not bare_array,
}
Expand Down
8 changes: 3 additions & 5 deletions flopy4/mf6/codec/reader/grammar/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ def valid_as_union(field: InputField) -> InputField:
"""Turn a ``valid=``-restricted scalar (e.g. STO's ``storage``, one of
STEADY-STATE/TRANSIENT) into a keyword union with one arm per value, so
it reuses the existing union grammar/dispatch instead of a second path.
A union never begins with its own name, so whether a tagged scalar's
name precedes the value is up to the caller (see ``union_field``).
"""
valid = getattr(field, "valid", None)
if not valid:
return field
return Union(
name=field.name,
tagged=field.tagged,
arms={str(v): Keyword(name=str(v)) for v in valid},
)
return Union(name=field.name, arms={str(v): Keyword(name=str(v)) for v in valid})


def keyword_literal(field: InputField) -> str:
Expand Down
43 changes: 41 additions & 2 deletions flopy4/mf6/codec/reader/grammar/generated/gwf-lak.lark
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ packagedata__fields: ((packagedatadata | open_close_redirect) _NL*)*
connectiondata__fields: ((connectiondatadata | open_close_redirect) _NL*)*
tables__fields: ((tablesdata | open_close_redirect) _NL*)*
outlets__fields: ((outletsdata | open_close_redirect) _NL*)*
period__fields: ((stress_period_data | open_close_redirect) _NL*)*
period__fields: ((status__rec__elem | stage__rec__elem | rainfall__rec__elem | evaporation__rec__elem | runoff__rec__elem | inflow__rec__elem | withdrawal__rec__elem | rate__rec__elem | invert__rec__elem | width__rec__elem | slope__rec__elem | rough__rec__elem | auxiliaryrecord__rec__elem | open_close_redirect) _NL*)*
auxiliary__val: "auxiliary"i word+ [_remark] _NL
boundnames__kw: "boundnames"i [_remark] _NL
print_input__kw: "print_input"i [_remark] _NL
Expand Down Expand Up @@ -71,8 +71,47 @@ dev_no_final_check__kw: "dev_no_final_check"i [_remark] _NL
nlakes__val: "nlakes"i integer [_remark] _NL
noutlets__val: "noutlets"i integer [_remark] _NL
ntables__val: "ntables"i integer [_remark] _NL
status__rec__elem: status__lakeno__val status__status__val [_remark] _NL
status__lakeno__val: integer
status__status__val: "status"i word
stage__rec__elem: stage__lakeno__val stage__stage__val [_remark] _NL
stage__lakeno__val: integer
stage__stage__val: "stage"i double
rainfall__rec__elem: rainfall__lakeno__val rainfall__rainfall__val [_remark] _NL
rainfall__lakeno__val: integer
rainfall__rainfall__val: "rainfall"i double
evaporation__rec__elem: evaporation__lakeno__val evaporation__evaporation__val [_remark] _NL
evaporation__lakeno__val: integer
evaporation__evaporation__val: "evaporation"i double
runoff__rec__elem: runoff__lakeno__val runoff__runoff__val [_remark] _NL
runoff__lakeno__val: integer
runoff__runoff__val: "runoff"i double
inflow__rec__elem: inflow__lakeno__val inflow__inflow__val [_remark] _NL
inflow__lakeno__val: integer
inflow__inflow__val: "inflow"i double
withdrawal__rec__elem: withdrawal__lakeno__val withdrawal__withdrawal__val [_remark] _NL
withdrawal__lakeno__val: integer
withdrawal__withdrawal__val: "withdrawal"i double
rate__rec__elem: rate__outletno__val rate__rate__val [_remark] _NL
rate__outletno__val: integer
rate__rate__val: "rate"i double
invert__rec__elem: invert__outletno__val invert__invert__val [_remark] _NL
invert__outletno__val: integer
invert__invert__val: "invert"i double
width__rec__elem: width__outletno__val width__width__val [_remark] _NL
width__outletno__val: integer
width__width__val: "width"i double
slope__rec__elem: slope__outletno__val slope__slope__val [_remark] _NL
slope__outletno__val: integer
slope__slope__val: "slope"i double
rough__rec__elem: rough__outletno__val rough__rough__val [_remark] _NL
rough__outletno__val: integer
rough__rough__val: "rough"i double
auxiliaryrecord__rec__elem: auxiliaryrecord__lakeno__val "auxiliary"i auxiliaryrecord__auxname__val auxiliaryrecord__auxval__val [_remark] _NL
auxiliaryrecord__lakeno__val: integer
auxiliaryrecord__auxname__val: word
auxiliaryrecord__auxval__val: double
packagedatadata: record+
connectiondatadata: record+
tablesdata: record+
outletsdata: record+
stress_period_data: record+
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ double_index: block_index
{% elif field.type == 'record' and field.children is not none -%}
{{ m.record_field(field_name, field, name) }}
{% elif field.type == 'union' and field.children is not none -%}
{{ m.union_field(field_name, field, name) }}
{{ m.union_field(field_name, field, name, field_name in block.tagged_valid) }}
{% elif field.type == 'file' -%}
{{ m.file_field(name, field) }}
{% else -%}
Expand Down
19 changes: 10 additions & 9 deletions flopy4/mf6/codec/reader/grammar/templates/macros.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@
{%- if child.type == 'keyword' -%}
!{{ rule(field_name, child_name, tags=['kw']) }}: {{ child|keyword_literal }}
{%- elif child.type == 'array' -%}
{{ rule(field_name, child_name, tags=['val']) }}: "{{ child.name }}"i {{ child|field_type }}
{{ rule(field_name, child_name, tags=['val']) }}: {% if child.tagged %}"{{ child.name }}"i {% endif %}{{ child|field_type }}
{%- else -%}
{{ rule(field_name, child_name, tags=['val']) }}: "{{ child.name }}"i {{ child.type }}
{{ rule(field_name, child_name, tags=['val']) }}: {% if child.tagged %}"{{ child.name }}"i {% endif %}{{ child.type }}
{%- endif %}

{% endfor -%}
Expand All @@ -119,16 +119,17 @@
{%- endfor -%}
{%- endmacro %}

{% macro union_field(field_name, field, name) -%}
{% macro union_field(field_name, field, name, tagged=false) -%}
{# The alternation must be parenthesized before appending `[_remark] _NL` --
`|` binds looser than concatenation, so an unparenthesized "A | B | C
[_remark] _NL" only requires the line ending after the *last* arm. A
`tagged true` union (e.g. NPF's alternative_cell_averaging, confirmed via
valid_as_union) additionally needs its own field-name keyword prefixed --
real files write "ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare
"LOGARITHMIC" -- unlike an untagged keystring (e.g. ocsetting's ALL/
FIRST/LAST), where the chosen value's own keyword is the whole field. #}
{{ name }}: {% if field.tagged %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL
tagged `valid` scalar made a union (`tagged`, e.g. NPF's
alternative_cell_averaging, via valid_as_union) also begins with its
own name -- real files write
"ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare "LOGARITHMIC" --
unlike a union of keywords (e.g. ocsetting's ALL/FIRST/LAST), where the
chosen value's own keyword is the whole field. #}
{{ name }}: {% if tagged %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL

{{ union_arms(field_name, field) }}
{%- endmacro %}
Expand Down
4 changes: 2 additions & 2 deletions flopy4/mf6/codec/writer/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ def dataset2list(value: xr.Dataset):
Yield records (tuples) from an `xarray.Dataset`.

If the first data variable is a string type, assume all are
string type. Then the dataset represents a keystring; yield
tuples of (name, *value). Otherwise, yield tuples: (*value)
string type. Then the dataset represents keyword settings;
yield tuples of (name, *value). Otherwise, yield tuples: (*value)
if no spatial dimensions, or (*cellid, *value) when spatial
dimensions are present.
"""
Expand Down
13 changes: 10 additions & 3 deletions flopy4/mf6/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,18 @@ def _resolve_child_name(


def _row_children(row: Any) -> "list[Component]":
"""The children in a list's row (LAK's ``ifno TAB6 FILEIN <file>``)."""
"""The children in a list's row (LAK's ``ifno TAB6 FILEIN <file>``),
including in its union arm (SFR's ``CROSS_SECTION TAB6 FILEIN``)."""
if isinstance(row, Component) or not attrs.has(type(row)):
return []
children = (getattr(row, f.name) for f in fields(type(row)) if f.metadata.get("child"))
return [c for c in children if isinstance(c, Component)]
children = []
for f in fields(type(row)):
value = getattr(row, f.name)
if f.metadata.get("child"):
children += [value] if isinstance(value, Component) else []
elif attrs.has(type(value)):
children += _row_children(value)
return children


def _period_row_children(value: Any) -> "list[Component]":
Expand Down
19 changes: 8 additions & 11 deletions flopy4/mf6/gwe/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,49 +50,45 @@ class Packagedata(Item):
@attrs.define
class Status(Item):
_keyword: ClassVar[str] = "status"
lakeno: int = field(index=True, fk="packagedata.lakeno")
status: str = field()

@attrs.define
class Temperature(Item):
_keyword: ClassVar[str] = "temperature"
lakeno: int = field(index=True, fk="packagedata.lakeno")
temperature: Union[float, TimeSeriesRef] = field(time_series=True)

@attrs.define
class Rainfall(Item):
_keyword: ClassVar[str] = "rainfall"
lakeno: int = field(index=True, fk="packagedata.lakeno")
rainfall: Union[float, TimeSeriesRef] = field(time_series=True)

@attrs.define
class Evaporation(Item):
_keyword: ClassVar[str] = "evaporation"
lakeno: int = field(index=True, fk="packagedata.lakeno")
evaporation: Union[float, TimeSeriesRef] = field(time_series=True)

@attrs.define
class Runoff(Item):
_keyword: ClassVar[str] = "runoff"
lakeno: int = field(index=True, fk="packagedata.lakeno")
runoff: Union[float, TimeSeriesRef] = field(time_series=True)

@attrs.define
class ExtInflow(Item):
_keyword: ClassVar[str] = "ext-inflow"
lakeno: int = field(index=True, fk="packagedata.lakeno")
ext_inflow: Union[float, TimeSeriesRef] = field(time_series=True)

@attrs.define
class Auxiliary(Item):
_keyword: ClassVar[str] = "auxiliary"
lakeno: int = field(index=True, fk="packagedata.lakeno")
auxname: str = field()
auxval: Union[float, TimeSeriesRef] = field(time_series=True)

_StressPeriodDataItem = (
Status | Temperature | Rainfall | Evaporation | Runoff | ExtInflow | Auxiliary
)
@attrs.define
class StressPeriodData(Item):
lakeno: int = field(index=True, fk="packagedata.lakeno")
laksetting: "Lke.Status | Lke.Temperature | Lke.Rainfall | Lke.Evaporation | Lke.Runoff | Lke.ExtInflow | Lke.Auxiliary" = field( # noqa: E501
arms=True
)

flow_package_name: Optional[str] = field(
default=None,
Expand Down Expand Up @@ -176,7 +172,7 @@ class Auxiliary(Item):
block="packagedata",
write_if_empty=True,
)
_stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field(
_stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field(
alias="stress_period_data",
default=None,
repr=False,
Expand All @@ -193,3 +189,4 @@ class Auxiliary(Item):
LkeRunoff = Lke.Runoff
LkeExtInflow = Lke.ExtInflow
LkeAuxiliary = Lke.Auxiliary
LkeStressPeriodData = Lke.StressPeriodData
21 changes: 3 additions & 18 deletions flopy4/mf6/gwe/oc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# autogenerated file, do not modify
from pathlib import Path
from typing import ClassVar, Optional
from typing import ClassVar, Literal, Optional

import attrs

Expand Down Expand Up @@ -28,18 +28,6 @@ class Temperatureprint(Record):
_extra_tokens: ClassVar[tuple[str, ...]] = ("PRINT_FORMAT",)
formatrecord: "Oc.Format" = attrs.field()

@attrs.define
class All(Item):
_keyword: ClassVar[str] = "all"

@attrs.define
class First(Item):
_keyword: ClassVar[str] = "first"

@attrs.define
class Last(Item):
_keyword: ClassVar[str] = "last"

@attrs.define
class Frequency(Item):
_keyword: ClassVar[str] = "frequency"
Expand All @@ -54,13 +42,13 @@ class Steps(Item):
class Save(Item):
_keyword: ClassVar[str] = "save"
rtype: str = field()
ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field()
ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True)

@attrs.define
class Print(Item):
_keyword: ClassVar[str] = "print"
rtype: str = field()
ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field()
ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True)

_StressPeriodDataItem = Save | Print

Expand Down Expand Up @@ -102,9 +90,6 @@ class Print(Item):
)


OcAll = Oc.All
OcFirst = Oc.First
OcLast = Oc.Last
OcFrequency = Oc.Frequency
OcSteps = Oc.Steps
OcSave = Oc.Save
Expand Down
4 changes: 2 additions & 2 deletions flopy4/mf6/gwf/lak.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# autogenerated file, do not modify
from pathlib import Path
from typing import ClassVar, Optional, Union
from typing import ClassVar, Literal, Optional, Union

import attrs
import numpy as np
Expand Down Expand Up @@ -59,7 +59,7 @@ class Connectiondata(Item):
iconn: int = field(index=True)
cellid: tuple = field(cellid=True)
claktype: str = field()
bedleak: str = field()
bedleak: Union[Literal["NONE"], float] = field(union=("keyword", "double"))
belev: float = field()
telev: float = field()
connlen: float = field()
Expand Down
Loading
Loading