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
35 changes: 23 additions & 12 deletions flopy4/mf6/_mixin_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,27 +225,38 @@ def _field_types(cls: type) -> dict[str, Any]:
return types


def _leaves(cls: type) -> list[type]:
"""A class's subclasses that have none of their own, or the class if
it has none."""
subs = cls.__subclasses__()
return [leaf for s in subs for leaf in _leaves(s)] if subs else [cls]


def _missing(cls: type, chain: list[str]) -> list[str]:
"""Where a chain read from ``cls`` hits an attribute that isn't there,
as ``Class.attr``. Past a field that can hold something else too (see
`_open`), an attribute only some of the classes have isn't missing."""
`_open`), an attribute only some of the classes have isn't missing.
An attribute a base class lacks is looked for on its leaf subclasses,
which the DFNs may give it (e.g. ``Solution.models``)."""
missing = []
at = {cls}
unchecked = False
for attr in chain:
following: set[type] = set()
following_unchecked = False
for c in at:
fields = _field_types(c) if attrs.has(c) else {}
if attr in fields:
following |= _classes(fields[attr])
following_unchecked |= _open(fields[attr])
elif hasattr(c, attr):
value = getattr(c, attr)
if isinstance(value, type) and attrs.has(value):
following.add(value)
elif not unchecked:
missing.append(f"{c.__qualname__}.{attr}")
for base in at:
on_base = hasattr(base, attr) or (attrs.has(base) and attr in _field_types(base))
for c in [base] if on_base else _leaves(base):
fields = _field_types(c) if attrs.has(c) else {}
if attr in fields:
following |= _classes(fields[attr])
following_unchecked |= _open(fields[attr])
elif hasattr(c, attr):
value = getattr(c, attr)
if isinstance(value, type) and attrs.has(value):
following.add(value)
elif not unchecked:
missing.append(f"{c.__qualname__}.{attr}")
at, unchecked = following, following_unchecked
return missing

Expand Down
15 changes: 10 additions & 5 deletions flopy4/mf6/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import TYPE_CHECKING, Any, ClassVar, Optional

import attrs
from attrs import fields
from attrs import Attribute, fields

from flopy4.dimensions import DimensionResolverMixin
from flopy4.mf6.constants import MF6
Expand Down Expand Up @@ -575,17 +575,22 @@ def _retarget_references(self) -> None:
if any(a is not b for a, b in zip(referenced(new), referenced(value))):
setattr(self, f.name, new)

def _reference_problem(self, field: Attribute, model: "Component") -> str | None:
"""What's wrong with reference field `field` referring to `model`
(one of the simulation's models), or None. Subclasses say what they
accept (e.g. the model types an exchange connects)."""
return None

def _reference_errors(self) -> list[str]:
"""What's wrong with this component's references: a required one
missing, one not among the simulation's models, or one its field's
check finds a problem with (see `flopy4.spec.reference`)."""
missing, one not among the simulation's models, or one
`_reference_problem` finds a problem with."""
errors = []
models = getattr(self._parent, "models", [])
for f in self._reference_fields():
value = self._resolved(f.name)
if value is None and f.metadata["required"]:
errors.append(f"{self.name} has no {f.name}") # type: ignore[attr-defined]
check = f.metadata["check"]
for v in value if isinstance(value, list) else [value]:
if v is None:
continue
Expand All @@ -594,7 +599,7 @@ def _reference_errors(self) -> list[str]:
f"{self.name} refers to model {reference_names(v)!r} in {f.name}, " # type: ignore[attr-defined]
"which isn't in the simulation"
)
elif check is not None and (problem := check(self, v)):
elif problem := self._reference_problem(f, v):
errors.append(f"{self.name} {problem}") # type: ignore[attr-defined]
return errors

Expand Down
15 changes: 11 additions & 4 deletions flopy4/mf6/converter/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def component_ftype(cls: type) -> str:
return f"{cls_name.upper()}6"


def _flat(values) -> list:
"""Reference fields' values as a row's terms, lists spread out."""
return [v for value in values for v in (value if isinstance(value, list) else [value])]


@define
class Binding:
"""A serializable representation of a component."""
Expand All @@ -37,11 +42,13 @@ def to_tuple(self) -> tuple[str, ...]:
@classmethod
def from_component(cls, component: Component) -> "Binding":
def _get_binding_terms(component: Component) -> tuple[str, ...] | None:
if isinstance(component, Exchange):
return tuple(reference_names([component.exgmnamea, component.exgmnameb]))
elif isinstance(component, Solution):
if isinstance(component, Solution):
# a lone solution listing none solves them all
return tuple(reference_names(component._solved_models()))
elif isinstance(component, (Model, Package)):
if refs := component._reference_fields():
# the reference fields are the row's terms, in order
return tuple(_flat(reference_names(getattr(component, f.name)) for f in refs))
if isinstance(component, (Model, Package)):
return (component.name,) # type: ignore
return None

Expand Down
20 changes: 10 additions & 10 deletions flopy4/mf6/converter/ingress/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,19 +568,19 @@ def _parse_readarray_period_block(


def _apply_binding_terms(child: Any, terms: list, models: list) -> None:
from flopy4.mf6.exchange import Exchange
from flopy4.mf6.solution import Solution

"""Set the child's reference fields from the rest of its row, in
order: a single one takes a term, a list the rest."""
# the models loaded so far, so a loaded exchange or solution holds
# them as a built one does; unknown names stay
found = resolve([str(t) for t in terms], models)
if isinstance(child, Exchange):
if len(found) > 0:
child.exgmnamea = found[0]
if len(found) > 1:
child.exgmnameb = found[1]
elif isinstance(child, Solution) and found:
child.models = found
for f in child._reference_fields():
if not found:
break
if f.metadata["many"]:
value, found = found, []
else:
value, found = found[0], found[1:]
setattr(child, f.name, value)


def _disambiguate_ga_variant(candidates: "list[type[Component]]", path: Path) -> "type[Component]":
Expand Down
4 changes: 4 additions & 0 deletions flopy4/mf6/ems.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

import attrs

from flopy4.mf6.model import Model
from flopy4.mf6.solution import Solution
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
class Ems(Solution):
dfn_name: ClassVar[str] = "sln-ems"

slntype: ClassVar[str] = "ems"

models: list[Model | str] = reference()
43 changes: 21 additions & 22 deletions flopy4/mf6/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,26 @@
from typing import TYPE_CHECKING, Optional

import attrs
from attrs import Attribute

from flopy4.mf6.package import Package
from flopy4.spec import reference

if TYPE_CHECKING:
from flopy4.mf6.model import Model


def _connects(side: int):
"""A check that a model on `side` (0 for a, 1 for b) is of the type the
exchange connects there (see `Exchange.model_types`)."""

def check(exg: "Exchange", model: "Model") -> "str | None":
mtype = exg.model_types[side]
if type(model).dfn_name.partition("-")[0] == mtype: # type: ignore[attr-defined]
return None
return (
f"({type(exg).__name__}) needs a {mtype.upper()} model {'ab'[side]}, "
f"not {model.name!r} ({type(model).__name__})" # type: ignore[attr-defined]
)

return check


@attrs.define(kw_only=True, slots=False)
class Exchange(Package, ABC):
"""An exchange. Its `exgmnamea` and `exgmnameb` (generated, from its
row in the simulation's name file) are the models it connects, as
objects or names, of the types in `model_types`. The simulation owns
its models; an exchange only refers to them."""

exgtype: Optional[type] = attrs.field(default=None) # type: ignore
exgfile: Optional[Path] = attrs.field(default=None) # type: ignore
exgmnamea: "Model | str | None" = reference(many=False, check=_connects(0), required=True)
"""The first model the exchange connects, as an object or a name. The
simulation owns its models; an exchange only refers to them."""
exgmnameb: "Model | str | None" = reference(many=False, check=_connects(1), required=True)
"""The second model the exchange connects, as an object or a name."""
if TYPE_CHECKING:
exgmnamea: "Model | str | None"
exgmnameb: "Model | str | None"

@property
def model_types(self) -> tuple[str, str]:
Expand All @@ -44,3 +31,15 @@ def model_types(self) -> tuple[str, str]:
letters.)"""
pair = type(self).dfn_name.partition("-")[2] # type: ignore[attr-defined]
return pair[:3], pair[3:]

def _reference_problem(self, field: Attribute, model) -> "str | None":
"""A problem if the model isn't of the type the exchange connects on
the field's side (its references are a's, then b's, in row order)."""
side = [f.name for f in self._reference_fields()].index(field.name)
mtype = self.model_types[side]
if type(model).dfn_name.partition("-")[0] == mtype:
return None
return (
f"({type(self).__name__}) needs a {mtype.upper()} model {'ab'[side]}, "
f"not {model.name!r} ({type(model).__name__})"
)
4 changes: 4 additions & 0 deletions flopy4/mf6/exg/chfgwf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

from flopy4.mf6.exchange import Exchange
from flopy4.mf6.item import Item
from flopy4.mf6.model import Model
from flopy4.mf6.spec import cellid_, child, double, integer, keyword, list_
from flopy4.mf6.utl.obs import Obs
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand All @@ -22,6 +24,8 @@ class Exchangedata(Item):
bedleak: float = double()
cfact: float = double()

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
print_input: bool = keyword(
default=False,
block="options",
Expand Down
4 changes: 4 additions & 0 deletions flopy4/mf6/exg/gwegwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from flopy4.mf6.exchange import Exchange
from flopy4.mf6.gwe.mve import Mve
from flopy4.mf6.item import Item
from flopy4.mf6.model import Model
from flopy4.mf6.spec import array, cellid_, child, double, integer, keyword, list_, string
from flopy4.mf6.utl.obs import Obs
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand All @@ -36,6 +38,8 @@ class Exchangedata(Item):
default=None, empty_is_none=True, optional=True, when="boundnames"
)

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
gwfmodelname1: str = string(
default=None,
block="options",
Expand Down
5 changes: 5 additions & 0 deletions flopy4/mf6/exg/gwfgwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import attrs

from flopy4.mf6.exchange import Exchange
from flopy4.mf6.model import Model
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
class Gwfgwe(Exchange):
dfn_name: ClassVar[str] = "exg-gwfgwe"

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
4 changes: 4 additions & 0 deletions flopy4/mf6/exg/gwfgwf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
from flopy4.mf6.gwf.gnc import Gnc
from flopy4.mf6.gwf.mvr import Mvr
from flopy4.mf6.item import Item
from flopy4.mf6.model import Model
from flopy4.mf6.record import Record
from flopy4.mf6.spec import array, cellid_, child, double, integer, keyword, list_, record, string
from flopy4.mf6.utl.obs import Obs
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand Down Expand Up @@ -43,6 +45,8 @@ class Exchangedata(Item):
default=None, empty_is_none=True, optional=True, when="boundnames"
)

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
auxiliary: Optional[NDArray[np.str_]] = array(
default=None,
converter=attrs.converters.optional(to_array(np.str_)),
Expand Down
5 changes: 5 additions & 0 deletions flopy4/mf6/exg/gwfgwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import attrs

from flopy4.mf6.exchange import Exchange
from flopy4.mf6.model import Model
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
class Gwfgwt(Exchange):
dfn_name: ClassVar[str] = "exg-gwfgwt"

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
5 changes: 5 additions & 0 deletions flopy4/mf6/exg/gwfprt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import attrs

from flopy4.mf6.exchange import Exchange
from flopy4.mf6.model import Model
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
class Gwfprt(Exchange):
dfn_name: ClassVar[str] = "exg-gwfprt"

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
4 changes: 4 additions & 0 deletions flopy4/mf6/exg/gwtgwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from flopy4.mf6.exchange import Exchange
from flopy4.mf6.gwt.mvt import Mvt
from flopy4.mf6.item import Item
from flopy4.mf6.model import Model
from flopy4.mf6.spec import array, cellid_, child, double, integer, keyword, list_, string
from flopy4.mf6.utl.obs import Obs
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand All @@ -36,6 +38,8 @@ class Exchangedata(Item):
default=None, empty_is_none=True, optional=True, when="boundnames"
)

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
gwfmodelname1: str = string(
default=None,
block="options",
Expand Down
4 changes: 4 additions & 0 deletions flopy4/mf6/exg/olfgwf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

from flopy4.mf6.exchange import Exchange
from flopy4.mf6.item import Item
from flopy4.mf6.model import Model
from flopy4.mf6.spec import cellid_, child, double, integer, keyword, list_
from flopy4.mf6.utl.obs import Obs
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand All @@ -22,6 +24,8 @@ class Exchangedata(Item):
bedleak: float = double()
cfact: float = double()

exgmnamea: Model | str | None = reference(many=False, required=True)
exgmnameb: Model | str | None = reference(many=False, required=True)
print_input: bool = keyword(
default=False,
block="options",
Expand Down
3 changes: 3 additions & 0 deletions flopy4/mf6/ims.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import attrs

from flopy4.mf6.model import Model
from flopy4.mf6.record import Record
from flopy4.mf6.solution import Solution
from flopy4.mf6.spec import double, file, integer, record, string
from flopy4.spec import reference


@attrs.define(kw_only=True, slots=False)
Expand All @@ -26,6 +28,7 @@ class Rclose(Record):
inner_rclose: float = double()
rclose_option: Optional[str] = string(default=None, optional=True)

models: list[Model | str] = reference()
print_option: Optional[str] = string(
default=None,
block="options",
Expand Down
Loading
Loading