Skip to content
Closed
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
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"tagged": false,
"dtype": "integer",
"index": true,
"node": true,
"shape": [
"numalphaj"
]
Expand Down
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ description = "is the array of CELLIDS for the contributing j cells, which contr
tagged = false
dtype = "integer"
index = true
node = true
shape = [
"numalphaj",
]
Expand Down
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ blocks:
tagged: false
dtype: integer
index: true
node: true
shape:
- numalphaj
alphasj:
Expand Down
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"tagged": false,
"dtype": "integer",
"index": true,
"node": true,
"shape": [
"numalphaj"
]
Expand Down
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ description = "is the array of CELLIDS for the contributing j cells, which contr
tagged = false
dtype = "integer"
index = true
node = true
shape = [
"numalphaj",
]
Expand Down
1 change: 1 addition & 0 deletions autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ blocks:
tagged: false
dtype: integer
index: true
node: true
shape:
- numalphaj
alphasj:
Expand Down
12 changes: 12 additions & 0 deletions autotest/dfns/test_migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,15 @@ def test_migrate_ts_shapes(dfn_dir):
def test_migrate_auxiliary_stays_self_sizing(dfn_dir):
component = _migrate_dev3(dfn_dir, "gwf-chd")
assert component.blocks["options"].fields["auxiliary"].shape == []


def test_migrate_gnc_node_refs(dfn_dir):
component = _migrate_dev3(dfn_dir, "gwf-gnc")
item = component.blocks["gncdata"].fields["gncdata"].item
assert item.fields["cellidn"].node
assert item.fields["cellidm"].node
cellidsj = item.fields["cellidsj"]
assert isinstance(cellidsj, v2.Array)
assert cellidsj.node
assert cellidsj.shape == ["numalphaj"]
assert not item.fields["alphasj"].node
42 changes: 42 additions & 0 deletions autotest/dfns/test_schema_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,45 @@ def test_dfns_validate_fk_in_union_item_scalar_arm():
arm = Integer(name="lakeno", fk="nosuchblock.lakeno")
with pytest.raises(ValueError, match="is not a list block"):
Dfns(components=_union_ctx(arm))


def _node_ctx(field):
item = Record(name="item", fields={field.name: field})
lst = List(name="gncdata", item=item)
block = Block(name="gncdata", fields={"gncdata": lst})
pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"gncdata": block})
gwf = Model(name="gwf-nam", blocks=None)
return pkg, gwf


@pytest.mark.parametrize(
"field",
[
Integer(name="cellidn", node=True),
Array(name="cellidsj", dtype="integer", node=True),
],
)
def test_dfns_validate_node_in_list_item(field):
gnc, gwf = _node_ctx(field)
spec = Dfns(components={"gwf-nam": gwf, "gwf-gnc": gnc})
assert "gwf-gnc" in spec.components


def test_array_node_requires_integer_dtype():
with pytest.raises(ValueError, match="node=True requires dtype='integer'"):
Array(name="alphasj", dtype="double", node=True)


@pytest.mark.parametrize(
"field",
[
Integer(name="cellid", node=True),
Array(name="cellids", dtype="integer", node=True),
],
)
def test_dfns_validate_node_outside_list_item(field):
block = Block(name="options", fields={field.name: field})
pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"options": block})
gwf = Model(name="gwf-nam", blocks=None)
with pytest.raises(ValueError, match="only valid on a column in a list item record"):
Dfns(components={"gwf-nam": gwf, "gwf-gnc": pkg})
7 changes: 6 additions & 1 deletion docs/md/dfnspec.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy
- [`time_series`](#time_series-3)
- [`index`](#index-1)
- [`fk`](#fk-2)
- [`node`](#node-1)
- [Record](#record)
- [Type-specific attributes](#type-specific-attributes-7)
- [`fields`](#fields-2)
Expand Down Expand Up @@ -406,7 +407,7 @@ Type `integer`.

###### `node`

`boolean (default: false)`. Marks this scalar as a grid cell reference, resolved from the parent model's grid (DIS/DISV/DISU) at runtime. Valid only on integer scalars that are columns in a list item record.
`boolean (default: false)`. Marks this scalar as a grid cell reference, resolved from the parent model's grid (DIS/DISV/DISU) at runtime. Valid only on integer scalars or integer arrays (see [Array `node`](#node-1)) that are columns in a list item record.

#### Double

Expand Down Expand Up @@ -468,6 +469,10 @@ A 1D array appearing as a subfield of a record is called an **inline array**. In

`string | null (default: null)`. Marks the array's (nonzero) elements as a foreign key: a per-grid-cell reference to a row (by `pk`) in another list, rather than the per-list-row reference a scalar `fk` expresses (e.g. a grid-wide array giving each cell's cross-section id, referencing the cross-section package's `packagedata`). Only valid when `dtype` is `"integer"`. Hierarchical path form only (`"[component.]block.field"`) — an array has no `fk_ref` counterpart, since it has no sibling record to carry a runtime component-selector field, and no `pk` counterpart, since it has no rows of its own to be a key of. See "Primary/foreign keys".

###### `node`

`boolean (default: false)`. Marks each of the array's elements as a grid cell reference, like the scalar [`node`](#node) (e.g. GNC's `cellidsj`, `numalphaj` cellids). Each element is one cellid, so the field spans `prod(shape) * ncelldim` values in the input file, where `ncelldim` is that of the grid referred to (3 for DIS, 2 for DISV, 1 for DISU) rather than part of `shape`. Only valid when `dtype` is `"integer"` and the array is a column in a list item record.

#### Record

Type `record`. Product type. In MF6 input files, records appear on a single line, except for a record that consists of a dynamic keyword (see [List](#list)) followed by an array: it is read like a tagged array whose tag is the key's value, i.e. the key on its own line, then the array in READARRAY format on the following lines. Record subfields may or may not be `tagged`. While blocks can be considered product types also, in the DFN specification only records are considered fields; blocks are considered named collections of related fields.
Expand Down
9 changes: 6 additions & 3 deletions modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,8 @@ def _mark_lonely_pk(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]:
return {**blocks, **updated}


# Integer columns in a list item record that reference a grid cell, resolved
# Integer columns (scalars, or arrays of cellids like GNC's `cellidsj`) in a
# list item record that reference a grid cell, resolved
# from the parent model's grid (DIS/DISV/DISU) at runtime, rather than a pk/fk
# relation to another list's row. v1 has no attribute that signals this
# (`numeric_index` only means "needs 1-based/0-based conversion", not "is a
Expand All @@ -502,7 +503,7 @@ def _mark_lonely_pk(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]:
"exg-gwfgwf": ("exchangedata", ("cellidm1", "cellidm2")),
"exg-gwtgwt": ("exchangedata", ("cellidm1", "cellidm2")),
"exg-olfgwf": ("exchangedata", ("cellidm1", "cellidm2")),
"gwf-gnc": ("gncdata", ("cellidm", "cellidn")),
"gwf-gnc": ("gncdata", ("cellidm", "cellidn", "cellidsj")),
}


Expand All @@ -526,7 +527,9 @@ def _mark_node_refs(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Bloc
updates = {
fname: f.model_copy(update={"node": True})
for fname in field_names
if isinstance(f := item.fields.get(fname), v2.Integer) and not f.node
if isinstance(f := item.fields.get(fname), (v2.Integer, v2.Array))
and (isinstance(f, v2.Integer) or f.dtype == "integer")
and not f.node
}
if not updates:
return blocks
Expand Down
5 changes: 5 additions & 0 deletions modflow_devtools/dfns/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@
],
"default": null,
"title": "Fk"
},
"node": {
"default": false,
"title": "Node",
"type": "boolean"
}
},
"required": [
Expand Down
38 changes: 38 additions & 0 deletions modflow_devtools/dfns/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,21 @@ class Array(InputFieldBase):
layered: bool = False
index: bool = False
fk: str | None = None
# Each element is a grid-cell reference (see `Integer.node`), so the field
# spans `prod(shape) * ncelldim` tokens, the cell width coming from the
# grid referred to rather than from the shape.
node: bool = False

@model_validator(mode="after")
def _check_index_dtype(self) -> "Array":
if self.index and self.dtype != "integer":
raise ValueError(
f"Array {self.name!r}: index=True requires dtype='integer', got {self.dtype!r}"
)
if self.node and self.dtype != "integer":
raise ValueError(
f"Array {self.name!r}: node=True requires dtype='integer', got {self.dtype!r}"
)
if self.fk is not None and self.dtype != "integer":
raise ValueError(
f"Array {self.name!r}: fk requires dtype='integer', got {self.dtype!r}"
Expand Down Expand Up @@ -1234,6 +1242,34 @@ def _check_fields(fields: dict, dynamic_key: "InputField | None" = None) -> None
_check_fields(block.fields)


def _validate_node_fields(component: "ComponentBase") -> None:
"""
A grid-cell reference (`node=True`, see `Integer.node`/`Array.node`) is
only meaningful as a column in a list item record, where each row names
its cell(s). The dtype constraint on arrays is checked by `Array` itself.
"""
if not component.blocks:
return

def _check(field: "InputField", in_item: bool) -> None:
if getattr(field, "node", False) and not in_item:
raise ValueError(
f"Field {field.name!r}: node=True is only valid on a column in a list item record"
)
if isinstance(field, List):
_check(field.item, True)
elif isinstance(field, Record):
for subfield in field.fields.values():
_check(subfield, in_item)
elif isinstance(field, Union):
for arm in field.arms.values():
_check(arm, in_item)

for block in component.blocks.values():
for field in block.fields.values():
_check(field, False)


def _validate_array_shapes(
component: "ComponentBase",
component_name: str,
Expand Down Expand Up @@ -1516,6 +1552,8 @@ def _validate_relations(self) -> "Dfns":
_resolve_derived_dims(component, self.dims(name))
for name, component in self.components.items():
_validate_fk_fields(component, self)
for name, component in self.components.items():
_validate_node_fields(component)
for name, component in self.components.items():
_validate_array_shapes(component, name, self)
for name, component in self.components.items():
Expand Down
Loading