diff --git a/flopy4/mf6/converter/ingress/structure.py b/flopy4/mf6/converter/ingress/structure.py index f3708c0e..01cd70a8 100644 --- a/flopy4/mf6/converter/ingress/structure.py +++ b/flopy4/mf6/converter/ingress/structure.py @@ -18,6 +18,7 @@ from flopy4.mf6.item import ( Item, RowContext, + conditional_fields, dim_lookup, item_list_type, item_list_words, @@ -55,6 +56,7 @@ def _parse_rows( context: LoadContext, sizes: "dict[str, int] | None" = None, boundnames: bool = False, + enabled: frozenset[str] = frozenset(), union_arm: str = "cellid", union_forms: Callable[[Mapping[str, Any]], Any] | None = None, load_child: Callable[[Any, str], Any] | None = None, @@ -69,7 +71,9 @@ def _parse_rows( ncelldim = context.ncelldim(classes[0]) if len(classes) == 1 else None if ncelldim is None: ncelldim = ncelldim_from_dims(dict(context.dims)) - row_context = RowContext(ncelldim=ncelldim, sizes=sizes or {}, boundnames=boundnames) + row_context = RowContext( + ncelldim=ncelldim, sizes=sizes or {}, boundnames=boundnames, enabled=enabled + ) if isinstance(item_cls, tuple): return parse_union_items( rows, item_cls, words=words, context=row_context, load_child=load_child @@ -1060,6 +1064,9 @@ def structure_component( if (n := dim_value(shape, lookup=lambda name: dim_lookup(kwargs, name))) is not None: sizes[shape] = n boundnames = bool(kwargs.get("boundnames", False)) + enabled = frozenset( + o for ic in item_types for o in conditional_fields(ic).values() if kwargs.get(o) + ) # The grid's dims give the cellids' width (see LoadContext.ncelldim). # Same "self dims, since a DimensionProvider has none threaded to it @@ -1091,6 +1098,7 @@ def load_child(f: Any, token: str) -> Any: item_cls, sizes=sizes, boundnames=boundnames, + enabled=enabled, context=rows_context, union_arm=union_arm, union_forms=union_forms, @@ -1116,6 +1124,7 @@ def load_child(f: Any, token: str) -> Any: item_cls, sizes=sizes, boundnames=boundnames, + enabled=enabled, context=rows_context, union_arm=union_arm, union_forms=union_forms, @@ -1152,6 +1161,7 @@ def load_child(f: Any, token: str) -> Any: period_item_cls, sizes=sizes, boundnames=boundnames, + enabled=enabled, context=rows_context, union_arm=union_arm, union_forms=union_forms, diff --git a/flopy4/mf6/gwf/evt.py b/flopy4/mf6/gwf/evt.py index e9b8c89f..00dc53f9 100644 --- a/flopy4/mf6/gwf/evt.py +++ b/flopy4/mf6/gwf/evt.py @@ -36,7 +36,7 @@ class StressPeriodData(Item): default=(), shape=("nseg-1",), time_series=True, optional=True ) petm0: Optional[Union[float, TimeSeriesRef]] = double( - default=None, time_series=True, optional=True + default=None, time_series=True, when="surf_rate_specified" ) aux: tuple[Union[float, TimeSeriesRef], ...] = array( default=(), shape=("auxiliary",), time_series=True, optional=True diff --git a/flopy4/mf6/item.py b/flopy4/mf6/item.py index 346667c2..62978165 100644 --- a/flopy4/mf6/item.py +++ b/flopy4/mf6/item.py @@ -127,6 +127,23 @@ def _has_boundname_field(cls: type) -> bool: return any(f.name == "boundname" for f in cast(type[Record], cls).fields()) +def conditional_fields(item_cls: "type[Item] | tuple[type[Item], ...]") -> dict[str, str]: + """An item class's fields an option turns on (the DFN's ``when``), by + name, to the option.""" + classes = item_cls if isinstance(item_cls, tuple) else (item_cls,) + return { + f.name: option + for c in classes + for f in cast(type[Record], c).fields() + if (option := f.metadata.get("when")) + } + + +def _may_be_absent(f: attrs.Attribute) -> bool: + """Whether a field may be missing from a row.""" + return bool(f.metadata.get("optional") or f.metadata.get("when")) + + @lru_cache(maxsize=None) def _union_arms( cls: type, type_str: str @@ -316,7 +333,7 @@ def solve(i: int, pos: int, form: "tuple[str, ...] | None") -> "tuple[int, list] if pos == len(tokens) or (form is not None and i == len(form)): # trailing optional columns may be absent, but not a form's ends = pos == len(tokens) and (i == len(form) if form else True) - absent = ends and fields[i].metadata.get("optional") + absent = ends and _may_be_absent(fields[i]) return (0, [None] * (len(fields) - i)) if absent else None best = None kinds = fields[i].metadata["union"] if form is None else (form[i],) @@ -374,11 +391,13 @@ class RowContext: exchange's models may be on different grids); None if not known. ``sizes`` gives the counts of the inline arrays the package sizes, by shape expression (aux, by ``auxiliary``); one not given isn't known. - ``boundnames`` is the package's option.""" + ``boundnames`` is the package's option. ``enabled`` are the set + options that turn fields on; None if not known.""" ncelldim: int | Mapping[str, int] | None = None sizes: Mapping[str, int] = attrs.field(factory=dict) boundnames: bool = False + enabled: frozenset[str] | None = None def cellid_width(self, f: attrs.Attribute) -> int | None: if isinstance(self.ncelldim, Mapping): @@ -482,8 +501,9 @@ def _layout( The boundname is there if the package's ``boundnames`` option is set (and a value is left for it). For Python values with it unset, one value more than the row's fields take is the boundname, and the - package then sets the option. An optional untagged field (EVT's petm0) - is there if the values make room for it. A width that isn't known (no + package then sets the option. A field an option turns on (EVT's petm0) + is there if the option is enabled, or, if that isn't known, if the + values make room for it. A width that isn't known (no grid yet) is the one the values make room for, if only one does; otherwise this raises, asking for the field as a tuple. Like MF6, a file row's extra tokens are ignored, if no layout uses them all. @@ -491,6 +511,13 @@ def _layout( n = len(values) slots = _slots(cls, file) has_boundname = _has_boundname_field(cls) + gated = conditional_fields(cls) + if context.enabled is None: + off: frozenset[str] = frozenset() + on: frozenset[str] = frozenset() + else: + off = frozenset(name for name, o in gated.items() if o not in context.enabled) + on = frozenset(gated) - off def cellid_widths(f: attrs.Attribute, unknown: dict) -> list[tuple[int, dict]]: if (w := context.cellid_width(f)) is not None: @@ -504,13 +531,19 @@ def flat(span: list) -> bool: """Whether values are a field's values, not whole fields.""" return file or not any(_is_seq(x) or isinstance(x, Record) for x in span) - def options(slot: _Slot, pos: int, end: int, out: dict, unknown: dict): + def fits(slot: _Slot, pos: int, end: int, out: dict, unknown: dict, conditional: bool): """Each way the slot can take values from ``pos``: the position after it, what it took, and the unknown widths it assumed.""" f = slot.field left = end - pos v = values[pos] if left else None - optional = f is not None and f.metadata.get("optional") + optional = f is not None and _may_be_absent(f) + if conditional and f is not None: + if f.name in off: + yield pos, None, unknown + return + if f.name in on: + optional = bool(f.metadata.get("optional")) if slot.kind == "keyword": if left and str(v).upper() == cast(type[Record], cls).keyword().upper(): yield pos + 1, None, unknown @@ -527,7 +560,7 @@ def options(slot: _Slot, pos: int, end: int, out: dict, unknown: dict): if file: yield end, ("unions", values[pos:end], slot.group), unknown return - required = sum(1 for g in slot.group if not g.metadata.get("optional")) + required = sum(1 for g in slot.group if not _may_be_absent(g)) for m in range(min(len(slot.group), left), required - 1, -1): yield pos + m, ("unions", values[pos : pos + m], slot.group), unknown return @@ -584,10 +617,12 @@ def options(slot: _Slot, pos: int, end: int, out: dict, unknown: dict): together = [ s.field.name for s in slots - if cast(type[Item], cls)._columns and s.kind == "value" and s.field.metadata.get("optional") + if cast(type[Item], cls)._columns and s.kind == "value" and _may_be_absent(s.field) ] - def walk(k: int, pos: int, end: int, out: dict, unknown: dict, surplus: bool): + def walk( + k: int, pos: int, end: int, out: dict, unknown: dict, surplus: bool, conditional: bool + ): if k == len(slots): if len({name in out for name in together}) > 1: return @@ -595,16 +630,16 @@ def walk(k: int, pos: int, end: int, out: dict, unknown: dict, surplus: bool): yield out, unknown return slot = slots[k] - for pos2, took, unknown2 in options(slot, pos, end, out, unknown): + for pos2, took, unknown2 in fits(slot, pos, end, out, unknown, conditional): out2 = out if took is None else {**out, slot.field.name: took} - yield from walk(k + 1, pos2, end, out2, unknown2, surplus) + yield from walk(k + 1, pos2, end, out2, unknown2, surplus, conditional) - def solve(boundname: list[bool], surplus: bool = False): + def solve(boundname: list[bool], surplus: bool = False, conditional: bool = True): for bn in boundname: end = n - bn if end < 0: continue - for out, unknown in walk(0, 0, end, {}, {}, surplus): + for out, unknown in walk(0, 0, end, {}, {}, surplus, conditional): yield ({**out, "boundname": ("bn", values[-1])} if bn else out), unknown fields = attrs.fields_dict(cls) # type: ignore[arg-type] @@ -612,9 +647,7 @@ def solve(boundname: list[bool], surplus: bool = False): def optionals(out: dict) -> int: """How many optional fields a layout has.""" return sum( - 1 - for k in out - if k == "boundname" or (k in fields and fields[k].metadata.get("optional")) + 1 for k in out if k == "boundname" or (k in fields and _may_be_absent(fields[k])) ) def first(solutions) -> "dict | None": @@ -636,8 +669,10 @@ def first(solutions) -> "dict | None": bn = [True, False] if has_boundname and context.boundnames else [False] out = first(solve(bn)) + if out is None and not file and (off or on): + out = first(solve(bn, conditional=False)) if out is None and not file and has_boundname and not context.boundnames: - out = first(solve([True])) + out = first(solve([True])) or first(solve([True], conditional=False)) if out is None and file: out = first(solve(bn, surplus=True)) if out is None: @@ -784,7 +819,7 @@ def to_tokens(self, *, ncelldim: int | Mapping[str, int] | None = None) -> tuple ) val = n if val is None and not (_is_one_cellid(f) and f.metadata.get("allow_zero")): - if _is_one_cellid(f) and not f.metadata.get("optional"): + if _is_one_cellid(f) and not _may_be_absent(f): raise ValueError(f"{cls.__name__}.{f.name}: no cellid given") continue if not keyword_emitted and not _is_row_key(f): diff --git a/flopy4/mf6/package.py b/flopy4/mf6/package.py index 584e285a..f6031d52 100644 --- a/flopy4/mf6/package.py +++ b/flopy4/mf6/package.py @@ -1,4 +1,6 @@ import operator +import re +import warnings from abc import ABC from pathlib import Path from typing import Any, Optional @@ -18,6 +20,7 @@ from flopy4.mf6.item import ( Item, RowContext, + conditional_fields, dim_lookup, inline_dim, item_list_type, @@ -60,6 +63,15 @@ } +_NUMBER = re.compile(r"[+-]?(\d+\.?\d*|\.\d+)([ed][+-]?\d+)?", re.IGNORECASE) + + +def _reads_as_number(text: str) -> bool: + """Whether MF6 could read a word as a number, with an E or a Fortran D + exponent.""" + return _NUMBER.fullmatch(text) is not None + + def _check_rows(dim: str, declared: int, nrows: int, bound: str | None) -> None: if not _BOUND_OPS[bound](nrows, declared): raise ValueError( @@ -365,7 +377,7 @@ def _row_context(self, item_cls: "type[Item] | tuple[type[Item], ...]") -> RowCo """What the layout of a row of ``item_cls`` given in Python depends on (see `Item.from_values`): the grid's ncelldim, if known (an exchange's two grids aren't, here), the sizes of the inline arrays - the package sizes, and the boundnames option. A size the rows can + the package sizes, and its enabled options. A size the rows can set (EVT's nseg, GNC's numalphaj) is solved from them if unset; one they can't (aux's, by the names in ``auxiliary``) is 0.""" fields = attrs.fields_dict(type(self)) # type: ignore[arg-type] @@ -388,8 +400,12 @@ def lookup(name: str) -> Any: ncelldim = self.resolve_dims("ncelldim").get("ncelldim") except ValueError: pass + enabled = frozenset(o for o in conditional_fields(item_cls).values() if values.get(o)) return RowContext( - ncelldim=ncelldim, sizes=sizes, boundnames=bool(getattr(self, "boundnames", False)) + ncelldim=ncelldim, + sizes=sizes, + boundnames=bool(getattr(self, "boundnames", False)), + enabled=enabled, ) def _coerce_item_list( @@ -415,8 +431,8 @@ def _coerce_item_list( takes. Ambiguous columnar dict-of-lists input isn't supported (no single arm to build columns from). - A row with a boundname sets the package's boundnames option, which - MF6 needs to read it. + A row with a boundname, or a field an option turns on, sets that + option, which MF6 needs to read it. """ context = self._row_context(item_cls) if isinstance(item_cls, tuple): @@ -439,9 +455,17 @@ def _coerce_item_list( items.append(item_cls(**row)) else: items.append(item_cls.from_values(row, context)) - if not context.boundnames and "boundnames" in attrs.fields_dict(type(self)): # type: ignore[arg-type] - if any(getattr(item, "boundname", None) for item in items): - object.__setattr__(self, "boundnames", True) + on = { + option + for name, option in conditional_fields(item_cls).items() + if any(getattr(item, name, None) is not None for item in items) + } + if any(getattr(item, "boundname", None) for item in items): + on.add("boundnames") + fields = attrs.fields_dict(type(self)) # type: ignore[arg-type] + for option in on: + if option in fields and not getattr(self, option): + object.__setattr__(self, option, True) return items def _normalize_griddata(self, fields, dims: dict) -> None: @@ -510,6 +534,7 @@ def write(self, format: str = MF6, context: Optional[WriteContext] = None) -> No self._sync_dims() self._check_tas_refs() self._check_ts_refs() + self._check_obs_ids() self._name_file_children() super().write(format=format, context=context) @@ -549,6 +574,30 @@ def _check_ts_refs(self) -> None: f"defines (defined: {sorted(defined) or 'none'})" ) + def _check_obs_ids(self) -> None: + """Warn if an observation names a boundname MF6 may read as a cell.""" + boundnames = set() + for f in attrs.fields(type(self)): # type: ignore[arg-type] + if item_list_type(f.type) is None: + continue + value = getattr(self, f.name) + for rows in (value if isinstance(value, dict) else {None: value}).values(): + boundnames |= {b for row in rows or [] if (b := getattr(row, "boundname", None))} + numeric = {b.lower() for b in boundnames if _reads_as_number(b)} + if not numeric: + return + for obs in self._children_of("utl-obs"): + for block in obs.continuous or []: + for row in block.continuous: + for id_ in (row.id_, row.id2): + if isinstance(id_, str) and id_.lower() in numeric: + warnings.warn( + f"{type(self).__name__}: observation {row.obsname!r} names " + f"boundname {id_!r}, which MF6 may read as a cell, not a " + f"boundname; give the boundaries a name that isn't a number", + stacklevel=3, + ) + def _series_names(self, dfn_name: str) -> set[str]: """The series names, lowercase, the package's TS6 or TAS6 files (``utl-ts``, ``utl-tas``) define.""" diff --git a/flopy4/mf6/spec.py b/flopy4/mf6/spec.py index 37ba9aaa..10c74f98 100644 --- a/flopy4/mf6/spec.py +++ b/flopy4/mf6/spec.py @@ -87,6 +87,7 @@ class FieldKwargs(TypedDict, total=False): optional: bool netcdf: bool tagged: bool + when: str | None def _pipe(*hooks): @@ -99,7 +100,7 @@ def _field(type_: str, kwargs: FieldKwargs, **attributes) -> Any: common ones in ``kwargs``) in its metadata where set.""" metadata = dict(kwargs.get("metadata") or {}) metadata["type"] = type_ - for key in ("block", "longname", "optional", "netcdf", "tagged"): + for key in ("block", "longname", "optional", "netcdf", "tagged", "when"): if kwargs.get(key): metadata[key] = kwargs[key] # type: ignore[literal-required] metadata.update({k: v for k, v in attributes.items() if v not in (None, False, ())}) diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index ed011ba4..55297994 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -871,24 +871,23 @@ def attr_column( *, optional: bool, time_series: bool = False, + when: str | None = None, default: str = "None", args: tuple[str, ...] = (), **kwargs, ) -> ItemColumn: """The generic rendering: ``name: T = fn(*args, meta)``, ``fn`` the field - function for the DFN type (see `flopy4.mf6.spec`), or, when optional, - ``Optional[T]`` with a ``None`` default (or ``T`` with ``default``).""" + function for the DFN type (see `flopy4.mf6.spec`), or, when optional + or ``when`` an option is set, ``Optional[T]`` with a ``None`` default + (or ``T`` with ``default``).""" meta = dict(meta) if time_series: meta["time_series"] = True if optional: - # Needed even for time_series fields: _n_fixed_tokens() (item.py) - # uses this to tell "always present" fixed columns apart from - # trailing columns that may be entirely absent from a given row - # (e.g. EVT's pxdp/petm/petm0, only written when - # surf_rate_specified) when inferring a variable-width cellid's - # element count from raw token counts. meta["optional"] = True + if when: + meta["when"] = when + optional = optional or bool(when) positional = [_dq(a) for a in args] if optional: annotation = py_type if default != "None" else f"Optional[{py_type}]" @@ -920,6 +919,7 @@ def value_column( *, optional: bool = False, time_series: bool = False, + when: str | None = None, ) -> ItemColumn: """A plain scalar. A time series value may also be a series reference.""" py_type = "Union[float, TimeSeriesRef]" if time_series else _DFN_PY.get(dfn_type, "float") @@ -930,6 +930,7 @@ def value_column( {}, optional=optional, time_series=time_series, + when=when, ) diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index acdd5568..507a6268 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -243,6 +243,7 @@ def _item_columns( f = col.field name = col.name dfn_type = _dfn_type_str(f) + when = getattr(f, "when", None) optional = bool(f.optional) or name in counts ts = bool(getattr(f, "time_series", False)) if isinstance(f, File): @@ -308,7 +309,11 @@ def _item_columns( dfn_type, optional=optional, time_series=ts, + when=when, ) + when = None + if when and name != "boundname": + raise NotImplementedError(f"column {name!r}: when={when!r} on a non-scalar column") items.append(item) return items diff --git a/pixi.lock b/pixi.lock index 5a3e363d..b24acfdb 100644 --- a/pixi.lock +++ b/pixi.lock @@ -106,7 +106,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -255,7 +255,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -404,7 +404,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -547,7 +547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -709,7 +709,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -987,7 +987,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1263,7 +1263,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1533,7 +1533,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1822,7 +1822,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2089,7 +2089,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2357,7 +2357,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2618,7 +2618,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2898,7 +2898,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3143,7 +3143,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3389,7 +3389,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -3629,7 +3629,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3887,7 +3887,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4129,7 +4129,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -4372,7 +4372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl @@ -4609,7 +4609,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl @@ -4863,7 +4863,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5107,7 +5107,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl @@ -5352,7 +5352,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5591,7 +5591,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -10852,7 +10852,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#873c5cac2b964841b43def94da867ce36bb9cd1b +- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9a1662ef87c006346b5769f856f730bad35f218f name: modflow-devtools version: 1.10.0.dev1 requires_dist: diff --git a/test/mf6/test_mf6_codec.py b/test/mf6/test_mf6_codec.py index 99d44b72..cb486cce 100644 --- a/test/mf6/test_mf6_codec.py +++ b/test/mf6/test_mf6_codec.py @@ -1857,16 +1857,76 @@ def test_row_boundname_by_option(boundnames): assert row.boundname == ("5" if boundnames else None) -@pytest.mark.parametrize("boundnames", [True, False]) -def test_row_optional_column_or_boundname(boundnames): - """EVT's petm0 is there if the row makes room for it, after the - boundname, which the option places.""" +@pytest.mark.parametrize( + "boundnames, surf_rate, tokens, petm0, boundname", + [ + # a row may leave out its boundname, but not petm0 + (True, True, [0.5], 0.5, None), + (True, True, [0.5, "w"], 0.5, "w"), + (True, False, [0.5], None, "0.5"), + (False, True, [0.5, "w"], 0.5, None), # MF6 ignores the extra token + (False, False, ["w"], None, None), + ], +) +def test_row_conditional_field_or_boundname(boundnames, surf_rate, tokens, petm0, boundname): + """EVT's petm0 is there iff SURF_RATE_SPECIFIED is set, and the + boundname if BOUNDNAMES is.""" from flopy4.mf6.gwf import Evt from flopy4.mf6.item import RowContext - context = RowContext(ncelldim=3, sizes={"nseg-1": 0, "auxiliary": 0}, boundnames=boundnames) - row = Evt.StressPeriodData.from_tokens([1, 1, 1, 10.0, 0.001, 2.0, 0.5], context) - assert (row.petm0, row.boundname) == ((None, "0.5") if boundnames else (0.5, None)) + context = RowContext( + ncelldim=3, + sizes={"nseg-1": 0, "auxiliary": 0}, + boundnames=boundnames, + enabled=frozenset({"surf_rate_specified"} if surf_rate else ()), + ) + row = Evt.StressPeriodData.from_tokens([1, 1, 1, 10.0, 0.001, 2.0, *tokens], context) + assert (row.petm0, row.boundname) == (petm0, boundname) + + +def test_row_conditional_field_sets_option(): + """A Python row with EVT's petm0 sets SURF_RATE_SPECIFIED, which MF6 + needs to read it; then a row's last value is its boundname.""" + from flopy4.mf6.gwf import Evt + + evt = Evt(stress_period_data={0: [((0, 0, 0), 10.0, 0.001, 2.0, 0.5)]}) + assert evt.stress_period_data[0][0].petm0 == 0.5 + assert evt.surf_rate_specified + assert not Evt(stress_period_data={0: [((0, 0, 0), 10.0, 0.001, 2.0)]}).surf_rate_specified + + evt = Evt( + boundnames=True, + surf_rate_specified=True, + stress_period_data={0: [((0, 0, 0), 10.0, 0.001, 2.0, 0.5, 7)]}, + ) + row = evt.stress_period_data[0][0] + assert (row.petm0, row.boundname) == (0.5, "7") + + +def test_evt_load_petm0_without_boundname(): + """With both options set, a file row leaving out its boundname keeps + its petm0.""" + from flopy4.mf6.converter.ingress.structure import structure_component + from flopy4.mf6.gwf import Evt + + text = """BEGIN OPTIONS + BOUNDNAMES + SURF_RATE_SPECIFIED +END OPTIONS + +BEGIN DIMENSIONS + MAXBOUND 2 + NSEG 1 +END DIMENSIONS + +BEGIN PERIOD 1 + 1 1 1 10.0 0.001 2.0 0.5 + 1 1 2 10.0 0.001 2.0 0.25 well +END PERIOD 1 +""" + evt = structure_component(loads(text), Evt) + rows = evt.stress_period_data[0] + assert [(r.petm0, r.boundname) for r in rows] == [(0.5, None), (0.25, "well")] @pytest.mark.parametrize("boundname", [None, "b"]) diff --git a/test/mf6/test_mf6_io_roundtrip.py b/test/mf6/test_mf6_io_roundtrip.py index ebb31621..bbe1d258 100644 --- a/test/mf6/test_mf6_io_roundtrip.py +++ b/test/mf6/test_mf6_io_roundtrip.py @@ -5,6 +5,7 @@ """ import math +import warnings from collections.abc import Mapping from pathlib import Path @@ -433,3 +434,43 @@ def test_quoted_multiword_boundname_and_obsname_round_trips(tmp_path): assert rghb.stress_period_data[0][0].boundname == "my bound" robs = rghb.obs.continuous[0].continuous[0] assert (robs.obsname, robs.id_) == ("my obs", "my bound") + + +@pytest.mark.parametrize( + "boundname, warns", + [("5", True), ("1.50", True), ("1d0", True), ("b5", False), ("inf", False)], +) +def test_obs_id_naming_numeric_boundname_warns(tmp_path, boundname, warns): + """MF6 may read an observation's id that looks like a number as a + cell, not a boundname, so writing one warns.""" + sim = Simulation( + tdis=Time(perlen=[1.0], nstp=[1], tsmult=[1.0], time_units="DAYS"), + workspace=tmp_path, + name="sim1", + solutiongroup=[Ims(filename="gwf.ims", models=["gwf1"])], + ) + dis = Dis(nlay=1, nrow=1, ncol=2, delr=1.0, delc=1.0, top=1.0, botm=0.0) + gwf = Gwf(parent=sim, dis=dis, name="gwf1", save_flows=True) + Ic(parent=gwf, strt=0.0) + Npf(parent=gwf, icelltype=0, k=1.0) + ghb = Ghb( + parent=gwf, + boundnames=True, + stress_period_data={0: [[(0, 0, 0), 1.0, 10.0, boundname]]}, + name="GHB-1", + ) + Obs( + parent=ghb, + continuous=[ + Obs.ContinuousBlock( + output=Obs.Output(obs_output_file_name="gwf1.ghb.obs.csv"), + continuous=[Obs.Continuous(obsname="o", obstype="GHB", id_=boundname)], + ) + ], + name="obs", + ) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + sim.write() + messages = [str(w.message) for w in caught if "MF6 may read as a cell" in str(w.message)] + assert len(messages) == warns