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
4 changes: 2 additions & 2 deletions docs/examples/circle.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,8 @@
"sim = flopy4.mf6.simulation.Simulation(\n",
" name=\"circle\",\n",
" tdis=tdis,\n",
" models={\"gwf\": gwf},\n",
" solutiongroup={\"ims\": ims},\n",
" models=[gwf],\n",
" solutiongroup=[ims],\n",
" workspace=workspace,\n",
")"
]
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ def plot_head_ugrid(head, cbc, workspace):
sim = flopy4.mf6.simulation.Simulation(
name="circle",
tdis=tdis,
models={"gwf": gwf},
solutiongroup={"ims": ims},
models=[gwf],
solutiongroup=[ims],
workspace=workspace,
)

Expand Down
5 changes: 3 additions & 2 deletions docs/examples/frenchman-flat.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@
"source": [
"# assemble GWF model from all packages defined above.\n",
"gwf = flopy4.mf6.gwf.Gwf(\n",
" name=\"ff\",\n",
" dis=dis,\n",
" ic=ic,\n",
" npf=npf,\n",
Expand Down Expand Up @@ -1094,8 +1095,8 @@
"sim = flopy4.mf6.simulation.Simulation(\n",
" name=\"ff\",\n",
" tdis=tdis,\n",
" models={\"ff\": gwf},\n",
" solutiongroup={\"ims\": ims},\n",
" models=[gwf],\n",
" solutiongroup=[ims],\n",
" workspace=workspace,\n",
")"
]
Expand Down
5 changes: 3 additions & 2 deletions docs/examples/frenchman-flat.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ def plot_head_ugrid(head, cbc, grid, workspace):

# assemble GWF model from all packages defined above.
gwf = flopy4.mf6.gwf.Gwf(
name="ff",
dis=dis,
ic=ic,
npf=npf,
Expand Down Expand Up @@ -662,8 +663,8 @@ def plot_head_ugrid(head, cbc, grid, workspace):
sim = flopy4.mf6.simulation.Simulation(
name="ff",
tdis=tdis,
models={"ff": gwf},
solutiongroup={"ims": ims},
models=[gwf],
solutiongroup=[ims],
workspace=workspace,
)

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/twri.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@
"sim = flopy4.mf6.simulation.Simulation(\n",
" name=\"twri\",\n",
" tdis=tdis,\n",
" models={\"gwf\": gwf},\n",
" solutiongroup={\"ims\": ims},\n",
" models=[gwf],\n",
" solutiongroup=[ims],\n",
" workspace=workspace,\n",
")"
]
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/twri.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ def plot_head(head, workspace):
sim = flopy4.mf6.simulation.Simulation(
name="twri",
tdis=tdis,
models={"gwf": gwf},
solutiongroup={"ims": ims},
models=[gwf],
solutiongroup=[ims],
workspace=workspace,
)

Expand Down
5 changes: 3 additions & 2 deletions docs/profile/diag_list_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def build_sim(n: int, ws: Path):
dims = {"nper": nper, "ncpl": nrow * n, **dict(grid.dataset.sizes)}
rch = flopy4.mf6.gwf.Rch(recharge={0: {(0, 0, c): 0.001 for c in range(n)}}, dims=dims)
gwf = flopy4.mf6.gwf.Gwf(
name="diag",
dis=grid,
ic=flopy4.mf6.gwf.Ic(strt=0.0, dims=dims),
npf=flopy4.mf6.gwf.Npf(
Expand All @@ -86,8 +87,8 @@ def build_sim(n: int, ws: Path):
tdis=flopy4.mf6.simulation.Tdis.from_time(
flopy4.mf6.utils.time.Time(perlen=[1.0], nstp=[1], tsmult=[1.0])
),
models={"diag": gwf},
solutiongroup={"ims": ims},
models=[gwf],
solutiongroup=[ims],
workspace=ws,
)

Expand Down
5 changes: 3 additions & 2 deletions docs/profile/ff_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ def build_flopy4_base(k, k33, ss):
for name, data in zip(_WEL_NAMES, _WEL_DICTS)
]
gwf = flopy4.mf6.gwf.Gwf(
name="ff",
dis=grid,
ic=ic,
npf=npf,
Expand Down Expand Up @@ -321,8 +322,8 @@ def main():
sim = flopy4.mf6.simulation.Simulation(
name="ff",
tdis=tdis,
models={"ff": gwf},
solutiongroup={"ims": ims},
models=[gwf],
solutiongroup=[ims],
workspace=ws,
)
if args.profile:
Expand Down
5 changes: 3 additions & 2 deletions docs/profile/test1000_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,13 @@ def make_base4():
return dis, ic, npf, sto, oc

def make_sim4(ws, gwf):
gwf.name = "test1000"
ws.mkdir(parents=True, exist_ok=True)
return flopy4.mf6.simulation.Simulation(
name="test1000",
tdis=flopy4.mf6.simulation.Tdis.from_time(time4),
models={"test1000": gwf},
solutiongroup={"ims": make_ims4()},
models=[gwf],
solutiongroup=[make_ims4(name="ims")],
workspace=ws,
)

Expand Down
5 changes: 3 additions & 2 deletions docs/profile/test1005_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ def make_base4():
return dis, ic, npf, sto, oc

def make_sim4(ws, gwf):
gwf.name = "test1005"
ws.mkdir(parents=True, exist_ok=True)
return flopy4.mf6.simulation.Simulation(
name="test1005",
tdis=flopy4.mf6.simulation.Tdis.from_time(time4),
models={"test1005": gwf},
solutiongroup={"ims": make_ims4()},
models=[gwf],
solutiongroup=[make_ims4(name="ims")],
workspace=ws,
)

Expand Down
17 changes: 7 additions & 10 deletions flopy4/attrs_xarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@
did, it would need a different name or a second, explicit exclusion
mechanism here.

Known limitation: a "dict"-kind child field (e.g. `Simulation.models`)
can't be reconstructed from a plain `DataTree` alone -- the dict's real
keys are caller-given names with no recoverable relationship to the
field name, unlike "list"-kind's positional `f"{field_name}{index}"`
convention. Namefile binding rows carry this information separately;
reconstruction should use that rather than guessing here. Likewise, a
Known limitation: a "dict"-kind child field can't be reconstructed
from a plain `DataTree` alone -- the dict's real keys are caller-given
names with no recoverable relationship to the field name, unlike
"list"-kind's positional `f"{field_name}{index}"` convention. Likewise, a
"list"-kind field whose element type is itself a `Union` of attrs
classes (e.g. `list[Union[Chd, Chdg]]`) isn't resolved to a concrete
arm.
Expand Down Expand Up @@ -325,8 +323,9 @@ def child_field_candidates(field: attrs.Attribute) -> "tuple[str, tuple[type, ..
annotation, or a type/collection-element that resolves to no
attrs-decorated candidate at all.

Kind is `"only"`, `"list"`, or `"dict"`, matching the child-collection
vocabulary used throughout `flopy4/mf6/converter/`.
Kind is `"only"` or `"list"`, matching the child-collection
vocabulary used throughout `flopy4/mf6/converter/`. Components keep
collections of children in lists, so a dict isn't a child field.
"""
tp = field.type
if tp is None or isinstance(tp, str):
Expand All @@ -347,8 +346,6 @@ def child_field_candidates(field: attrs.Attribute) -> "tuple[str, tuple[type, ..
candidates = tuple(a for a in get_args(elem) if attrs.has(a))
return ("list", candidates) if candidates else None
return ("list", (elem,)) if attrs.has(elem) else None
if origin is dict and len(args) == 2 and attrs.has(args[1]):
return ("dict", (args[1],))
return None


Expand Down
87 changes: 41 additions & 46 deletions flopy4/mf6/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,21 @@ def _is_default_child_name(child: "Component") -> bool:
return child.name == type(child).__name__.lower() # type: ignore[attr-defined]


def _resolve_child_name(used: "set[str]", kind: str, field_name: str, child: "Component") -> str:
def _names_by_class(candidates: "tuple[type, ...]") -> bool:
"""Whether a list field's unnamed children are named after their own
class rather than the field: true for a field of an abstract base
(`Simulation.models`, a list of `Model`), whose name says nothing
about which kind of child it holds."""
return not any("dfn_name" in c.__dict__ for c in candidates)


def _resolve_child_name(
used: "set[str]",
kind: str,
field_name: str,
child: "Component",
by_class: bool = False,
) -> str:
"""Resolve the name `child` should be attached under (stored as its
own `.name`), given the set of names already claimed by any of the
parent's other children (`used`).
Expand All @@ -85,8 +99,10 @@ def _resolve_child_name(used: "set[str]", kind: str, field_name: str, child: "Co
field pair like
`chd: list[Union[Chd, Chdg]]` sharing one sequence, since both arms
share one real MF6 namefile ftype (see `converter/binding.py`'s
`component_ftype()`). "dict"-kind isn't handled here -- its name is
the mapping key itself, resolved by the caller.
`component_ftype()`). With `by_class` (see `_names_by_class()`), an
unnamed child keeps its class-name default if it's free, else gets
`f"{name}{i}"` from 1, e.g. "gwf", "gwf1". The default can't be told
apart from the same name given explicitly, so it never collides.
"""
if kind not in ("only", "list"):
raise TypeError(f"Bad child collection kind '{kind}'")
Expand All @@ -99,6 +115,14 @@ def _resolve_child_name(used: "set[str]", kind: str, field_name: str, child: "Co
return child.name # type: ignore[attr-defined]
if kind == "only":
return field_name
if by_class:
name = child.name # type: ignore[attr-defined]
if name not in used:
return name
i = 1
while f"{name}{i}" in used:
i += 1
return f"{name}{i}"
i = 0
while f"{field_name}{i}" in used:
i += 1
Expand Down Expand Up @@ -258,7 +282,7 @@ def _children(self) -> "dict[str, Component]":
invalidation.

Re-runs `_set_child_parents()` first (idempotent) so every reader
sees correctly-named children even if a list/dict field was
sees correctly-named children even if a list field was
reassigned via plain attribute set (`gwf.wel = [...]`) rather than
construction or `__setitem__` -- the one case that otherwise skips
naming, leaving siblings collided on the shared class-name default
Expand Down Expand Up @@ -294,15 +318,11 @@ def _children(self) -> "dict[str, Component]":
result[child.name] = child
for c in _row_children(child):
result[c.name] = c
elif kind == "dict":
for child in value.values():
if isinstance(child, Component):
result[child.name] = child
return result

def _set_child_parents(self) -> None:
"""Stamp `_parent` on every already-populated Component-typed
field (single, list, or dict); see `_parent`'s own docstring
field (single or list); see `_parent`'s own docstring
above. Also resolves and stamps each child's `.name` --
`_resolve_child_name()`'s top-down counterpart to
`_attach_to_parent_field()`'s bottom-up one.
Expand All @@ -329,33 +349,25 @@ def _set_child_parents(self) -> None:
value = getattr(self, f.name, None)
if value is None:
continue
kind, _ = spec
kind, candidates = spec

if kind == "only":
if isinstance(value, Component):
value.__dict__["_parent"] = self
value.name = _resolve_child_name(used, kind, f.name, value) # type: ignore[attr-defined]
used.add(value.name) # type: ignore[attr-defined]
elif kind == "list":
by_class = _names_by_class(candidates)
for child in value:
for c in [child] if isinstance(child, Component) else _row_children(child):
if id(c) in stamped:
continue
stamped.add(id(c))
c.__dict__["_parent"] = self
c.name = _resolve_child_name(used, kind, f.name, c) # type: ignore[attr-defined]
c.name = _resolve_child_name( # type: ignore[attr-defined]
used, kind, f.name, c, by_class=by_class and c is child
)
used.add(c.name) # type: ignore[attr-defined]
elif kind == "dict":
for key, child in value.items():
if isinstance(child, Component):
child.__dict__["_parent"] = self
if key in used:
raise ValueError(
f"Child name '{key}' collides with an "
"existing child on the same parent."
)
child.name = key # type: ignore[attr-defined]
used.add(child.name) # type: ignore[attr-defined]

@property
def path(self) -> Path:
Expand Down Expand Up @@ -421,19 +433,14 @@ def _attach_to_parent_field(self, parent: "Component") -> None:
self.name = _resolve_child_name(used, kind, target_field.name, self) # type: ignore[attr-defined]
setattr(parent, target_field.name, self)
elif kind == "list":
self.name = _resolve_child_name(used, kind, target_field.name, self) # type: ignore[attr-defined]
from flopy4.attrs_xarray import child_field_candidates

spec = child_field_candidates(target_field)
assert spec is not None
self.name = _resolve_child_name( # type: ignore[attr-defined]
used, kind, target_field.name, self, by_class=_names_by_class(spec[1])
)
getattr(parent, target_field.name).append(self)
elif kind == "dict":
# No positional auto-key to fall back on for an unnamed child,
# unlike "only"/"list" -- see `_set_child_parents`'s "dict"
# branch: the child's own `.name` (explicit, or its
# class-name default) is the key.
key = self.name # type: ignore[attr-defined]
if key in used:
raise ValueError(
f"Child name '{key}' collides with an existing child on the same parent."
)
getattr(parent, target_field.name)[key] = self

@classmethod
def __attrs_init_subclass__(cls):
Expand Down Expand Up @@ -482,12 +489,6 @@ def __setitem__(self, key, value):
value.__dict__["_parent"] = self
current[i] = value
return
elif kind == "dict":
if current and key in current:
value.name = key # type: ignore[attr-defined]
value.__dict__["_parent"] = self
current[key] = value
return

match = _find_child_field(type(self), type(value))
if match is None:
Expand All @@ -499,8 +500,6 @@ def __setitem__(self, key, value):
setattr(self, target_field.name, value)
elif kind == "list":
getattr(self, target_field.name).append(value)
elif kind == "dict":
getattr(self, target_field.name)[key] = value

def __delitem__(self, key):
"""Detach the child named `key`, from whatever field/slot
Expand All @@ -522,10 +521,6 @@ def __delitem__(self, key):
if isinstance(child, Component) and child.name == key: # type: ignore[attr-defined]
del value[i]
return
elif kind == "dict":
if value and key in value:
del value[key]
return
raise KeyError(key)

def __iter__(self):
Expand Down
6 changes: 0 additions & 6 deletions flopy4/mf6/converter/egress/unstructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,6 @@ def _make_binding_blocks(value: Component) -> dict[str, dict[str, list[tuple[str
match child:
case Component():
blocks[block_name][child_name] = [Binding.from_component(child).to_tuple()]
case Mapping():
bindings = [
Binding.from_component(c).to_tuple() for c in child.values() if c is not None
]
if bindings:
blocks[block_name][child_name] = bindings
case Iterable():
bindings = [Binding.from_component(c).to_tuple() for c in child if c is not None]
if bindings:
Expand Down
Loading
Loading