Skip to content

several things - #348

Merged
wpbonelli merged 13 commits into
modflowpy:developfrom
wpbonelli:dfns-dev3
Aug 21, 2026
Merged

several things#348
wpbonelli merged 13 commits into
modflowpy:developfrom
wpbonelli:dfns-dev3

Conversation

@wpbonelli

@wpbonelli wpbonelli commented Aug 21, 2026

Copy link
Copy Markdown
Member
  • migrate code gen off the legacy DFN schema to the latest schema (2.0.0.dev3)
  • reimplement list based input: replace np.recarray + Schema/Column machinery with a Row mixin
  • fix two parsing bugs (synthetic aux field ordering, greedy optional-column consumption in from_row)
  • make recursive component loading work
  • move namefile subcomponent binding module binding.py into converter/
  • add dfn_name attribute to components
  • rework/rename component name/type registries

wpbonelli and others added 11 commits August 19, 2026 07:55
Rewrites flopy4/mf6/utils/codegen/filters.py and make.py to source from
modflow_devtools.dfns (pydantic, schema 2.0.0.dev3) instead of the legacy
flat-TypedDict modflow_devtools.dfn schema, and flips cli.py's sync path
to dev3. Bumps the modflow-devtools pin (pixi.lock) to pick up the pk/fk
backfill and other fixes this migration depends on.

Also merges the originally-separate Phase 0.6b (reflect DFN structure
literally) and Phase 0.7 (real union field type) work into this pass
where the runtime already supports it: OC's per-rtype table is now read
from the schema's real rtype.valid instead of a hardcoded table, and
LAK/LKE/LKT/SFR-style keystring period settings are detected directly
from the schema's Union arms instead of a hand-maintained override list.
A faithful typed-union representation (validated separately, not wired
in) is deferred to a follow-up phase once the Row migration (Phase 0.6)
lands, since structure.py/unstructure.py only support the flat Schema/
Column/role vocabulary today.

Regenerated all existing packages against the new pipeline and fixed
what the resulting diff surfaced: dropped `numeric_index`/`optional`
migration gaps in devtools (logged upstream, with local overrides as a
stopgap), a few flopy4-side misclassifications (string pk/fk shouldn't
get numeric index conversion, bare period scalars need the same
fill-forward wrapping as period lists), and one duplicate-field bug from
a now-stale override. See dev3-migration-gaps.md for the full rundown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the np.recarray + Schema/Column-driven ingress/egress machinery
with a Row mixin (flopy4/mf6/row.py): generated Row/record classes now
carry their own field()-level metadata (pk/fk/cellid/tagged/time_series/
prefix/inout) and are the single source of truth for (un)structuring list
input, matching the pattern record.py's Record mixin already used for
compound records. package.py, converter/ingress/structure.py, and
converter/egress/unstructure.py are rewritten to introspect Row classes
directly via attrs.fields() instead of a separate Schema/Column lookup;
flopy4/mf6/schema.py and its standalone unit tests are removed outright
since nothing else references them.

Also fixes two real parsing bugs surfaced along the way: row_class()
codegen was placing the synthetic `aux` field ahead of a package's own
optional non-aux columns (wrong relative to the real DFN token order,
e.g. EVT's pxdp/petm/petm0), and Row.from_row() greedily consumed tokens
for optional columns without knowing whether they were actually present
in a given row, misaligning subsequent fields; from_row() now infers how
many trailing optional columns are present from the remaining token
budget, the same way naux/ncelldim are already inferred.

Regenerates all 64 flopy4.mf6 component modules from the fixed codegen
and ports the hand-written tdis.py/disv.py (gwf/gwt/gwe/prt) files off
Schema/Column onto Row. Full suite is green except two pre-existing,
unrelated integration-test failures traced to xattree mangling an
explicit multi_package `name=` (e.g. "LAK-1" -> "LAK-10"), reproduced in
isolation with no Row/Column involvement.

Also commits the working planning docs this migration is tracked
against (namefile-load-plan.md, sync-plan.md, protocols-plan.md,
list-design.md, dev3-migration-gaps.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Simulation.load("mfsim.nam")/Gwf.load("model.nam") now recursively
resolve real MF6 namefiles instead of silently dropping packages/models/
exchanges/solutiongroup rows. Adds an FTYPES registry (component.py) and
_resolve_bindings() (converter/ingress/structure.py), which finds a
component's child-Component fields via xattree.get_xatspec().children
(mirroring unstructure.py's egress-side _make_binding_blocks), resolves
each namefile row's ftype token to a target class, and recursively loads
it -- merged into structure_component()'s existing single cls(**kwargs)
call rather than a separate post-construction attach step. Dimension
propagation (dis loaded before npf/ic/sto/buy in the same block, dims
threaded into their Package.load(dims=...) calls) is a small local
ordering rule inside _resolve_bindings, not a dimensions.py change.

G/A-variant packages (Chdg, Rcha, etc.) share their base's namefile
ftype -- confirmed against real MF6 source (gwf.f90's package-type
dispatch has no separate case for them), not legacy flopy's
dfn_file_name/_package_type, which describe DFN/class identity rather
than the namefile-level contract. Since this means Union[Chd, Chdg]-style
fields can't be disambiguated by token, _resolve_bindings peeks the
referenced file's OPTIONS block for the READASARRAYS/READARRAYGRID
marker instead, the same signal MF6 itself uses.

Also fixes a dead code path: Component.load()/Context.load() were
routed through converter.structure(), a bare cattrs.Converter with no
structure hook registered for the abstract Component base (never
worked; only mocked-loader tests exercised it). _load_mf6 now uses the
same structure_component() pipeline Package.load() already used
successfully, and the manual child-loading loops are dropped in favor
of children arriving already resolved from construction.

Known limitation: a binding row's pname isn't threaded through to the
loaded child's name (loaded children get xattree's default auto-
assigned name); Solution.models/Exchange.exgmnamea/exgmnameb are,
since those are real semantic data recoverable only from the row.

Renames namefile-load-plan.md to mf6-object-model-plan.md (scope grew
beyond namefile loading into the DFN-schema-migration prerequisite work
that turned out to gate it) and updates its Phase 0.6/0.6a/0.6b/0.7
status -- those had already landed but the doc, added to git in the
same commit that finished them, was never updated to say so. Marks
Phase 1 done with the above. Updates CLAUDE.md's reference to the
renamed file and its own description of current status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb3D6Df2Nh84MaJsmcvH3r
CLAUDE.md and the root-level *-plan.md/*-gaps.md/list-design.md planning
docs are local working notes, not meant to be version-controlled -- git rm
--cached (not git rm), files stay in the working tree, and .gitignore
keeps them from being re-added by accident.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb3D6Df2Nh84MaJsmcvH3r
Follow-up to c8ca43c -- the .gitignore edit didn't make it into that
commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb3D6Df2Nh84MaJsmcvH3r
Component.load()/Context.load()/Package.load()/structure_component() all
gain an optional name= param, threaded from a binding row's pname by
_resolve_bindings. This closes the "known limitation" from the previous
commit for dict-kind fields (Simulation.models/exchanges/solutions):
xattree reconciles a dict-kind child's .name to the key it's attached
under, so using the row's pname as that key (instead of fname, which
turned out not to be purely cosmetic as assumed) makes a custom pname
round-trip correctly -- confirmed by test.

It does not help list-kind (chd, wel, ...) or only-kind (dis, ic, npf,
...) package fields: xattree reconciles those to a field-derived name
regardless of what's passed to the constructor, confirmed true even at
write time (Chd(name="custom") already becomes "chd0" on attachment,
before any of this code runs). Not a load-side gap this closes without
touching xattree's own child-attachment convention, which is out of
scope here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb3D6Df2Nh84MaJsmcvH3r
…torage

binding.py maps the Component object model to/from MF6 wire-format
tuples, which is what converter/ (structure.py/unstructure.py) already
does on the ingress/egress sides of this same job -- codec/ stays a pure
syntax layer with no Component knowledge.

COMPONENTS and FTYPES each stored two entries per class (a plain key and
a model-qualified key), with inconsistent tie-break behavior on
collision (last-write-wins vs setdefault's first-write-wins). Both
registries now store a single fully-qualified key per class, with
lookup_component()/lookup_ftype() resolving a qualified name, an
unprefixed name, or a best-effort bare name (returning None rather than
an arbitrary class when more than one model shares it).
Codegen already computed the canonical DFN name (e.g. "gwf-ic") per
component but discarded it after building the class name/output path.
Emit it as a ClassVar on every generated class instead, and have
FNAMES register a class only when it declares its own dfn_name --
this replaces the module-path/class-name reconstruction in
__attrs_init_subclass__ and naturally excludes abstract bases
(Package, Context, Model, Exchange, Solution, DisBase) without the
"ABC in cls.__bases__" check that missed DisBase.

The handful of hand-written classes (Simulation, Tdis, Gwf/Gwt/Gwe/Prt,
per-model Dis/Disv, the GWF exchanges) declare dfn_name by hand using
their real DFN identity. Backfilled dfn_name onto the 55 already-
generated modules from the current registry, catching one real
mismatch along the way: Ims/Ems were keyed as bare "ims"/"ems" under
the old module-path scheme, but their DFN names are "sln-ims"/"sln-ems".

get_ftypes() now qualifies a ftype token with a model prefix only when
the bare token actually collides across classes, rather than always
qualifying based on a class's own dfn_name -- naively doing the latter
double-prefixed singleton tokens like Gwf's "gwf6" into "gwf-gwf6".
@wpbonelli wpbonelli added this to the MVP milestone Aug 21, 2026
wpbonelli and others added 2 commits August 21, 2026 06:34
Every case in this file exercised infrastructure that no longer exists:
Row.__iter__ (Row classes don't implement it anymore), Package's old
np.recarray coercion (stress_period_data/packagedata are now list[Row]),
and dtype-based recarray column access (spd["head"][i]). Equivalent
coverage of the current list[Row] API (all input styles, multi-row/
multi-period, aux, boundnames, static block rows) already lives in
test_mf6_codec.py, test_converter_structure.py, test_dataframe_api.py,
and test_mf6_component.py, updated in the prior commit. Full suite now
runs clean with no exclusions: 629 passed, 2 pre-existing/unrelated
failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…E tests

Positional tuples bind by Row field declaration order (aux before
boundname); the two new tests had them swapped, writing the boundname
string into the aux slot and the aux value into the boundname slot.
That malformed PACKAGEDATA block was crashing MF6 with SIGSEGV.
@wpbonelli
wpbonelli marked this pull request as ready for review August 21, 2026 15:13
@wpbonelli
wpbonelli merged commit 1e56bae into modflowpy:develop Aug 21, 2026
16 checks passed
@wpbonelli
wpbonelli deleted the dfns-dev3 branch August 21, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant