diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json index 133305fd..250b7389 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json @@ -42,17 +42,59 @@ "tagged": false }, "id": { - "type": "string", + "type": "union", "longname": "time", "description": "Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be a boundary name. Otherwise ID is a cellid. If the model discretization is type DIS, cellid is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number).", - "tagged": false + "tagged": false, + "arms": { + "cellid": { + "type": "array", + "tagged": false, + "dtype": "integer", + "index": true, + "cellid": true, + "shape": [ + "ncelldim" + ] + }, + "index": { + "type": "integer", + "tagged": false, + "index": true + }, + "boundname": { + "type": "string", + "tagged": false + } + } }, "id2": { - "type": "string", + "type": "union", "longname": "time", "description": "Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three observation types of the LAK Package, for two observation types of the MAW Package, and one observation type of the UZF Package.", "optional": true, - "tagged": false + "tagged": false, + "arms": { + "cellid": { + "type": "array", + "tagged": false, + "dtype": "integer", + "index": true, + "cellid": true, + "shape": [ + "ncelldim" + ] + }, + "index": { + "type": "integer", + "tagged": false, + "index": true + }, + "boundname": { + "type": "string", + "tagged": false + } + } } } } diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml index d879bfef..9b414aef 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml @@ -36,18 +36,56 @@ description = "a string of characters used to identify the observation type." tagged = false [blocks.continuous.fields.continuous.item.fields.id] -type = "string" +type = "union" longname = "time" description = "Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be a boundary name. Otherwise ID is a cellid. If the model discretization is type DIS, cellid is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number)." tagged = false -[blocks.continuous.fields.continuous.item.fields.id2] +[blocks.continuous.fields.continuous.item.fields.id.arms.cellid] +type = "array" +tagged = false +dtype = "integer" +index = true +cellid = true +shape = [ + "ncelldim", +] + +[blocks.continuous.fields.continuous.item.fields.id.arms.index] +type = "integer" +tagged = false +index = true + +[blocks.continuous.fields.continuous.item.fields.id.arms.boundname] type = "string" +tagged = false + +[blocks.continuous.fields.continuous.item.fields.id2] +type = "union" longname = "time" description = "Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three observation types of the LAK Package, for two observation types of the MAW Package, and one observation type of the UZF Package." optional = true tagged = false +[blocks.continuous.fields.continuous.item.fields.id2.arms.cellid] +type = "array" +tagged = false +dtype = "integer" +index = true +cellid = true +shape = [ + "ncelldim", +] + +[blocks.continuous.fields.continuous.item.fields.id2.arms.index] +type = "integer" +tagged = false +index = true + +[blocks.continuous.fields.continuous.item.fields.id2.arms.boundname] +type = "string" +tagged = false + [blocks.continuous.header.field] name = "output" type = "record" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml index 169f22d6..e653c198 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml @@ -45,7 +45,7 @@ blocks: description: a string of characters used to identify the observation type. tagged: false id: - type: string + type: union longname: time description: Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be @@ -53,8 +53,24 @@ blocks: is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number). tagged: false + arms: + cellid: + type: array + tagged: false + dtype: integer + index: true + cellid: true + shape: + - ncelldim + index: + type: integer + tagged: false + index: true + boundname: + type: string + tagged: false id2: - type: string + type: union longname: time description: Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three @@ -62,6 +78,22 @@ blocks: one observation type of the UZF Package. optional: true tagged: false + arms: + cellid: + type: array + tagged: false + dtype: integer + index: true + cellid: true + shape: + - ncelldim + index: + type: integer + tagged: false + index: true + boundname: + type: string + tagged: false header: field: name: output diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json index 5497f2b5..7d9cd9e3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json @@ -42,17 +42,59 @@ "tagged": false }, "id": { - "type": "string", + "type": "union", "longname": "time", "description": "Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be a boundary name. Otherwise ID is a cellid. If the model discretization is type DIS, cellid is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number).", - "tagged": false + "tagged": false, + "arms": { + "cellid": { + "type": "array", + "tagged": false, + "dtype": "integer", + "index": true, + "cellid": true, + "shape": [ + "ncelldim" + ] + }, + "index": { + "type": "integer", + "tagged": false, + "index": true + }, + "boundname": { + "type": "string", + "tagged": false + } + } }, "id2": { - "type": "string", + "type": "union", "longname": "time", "description": "Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three observation types of the LAK Package, for two observation types of the MAW Package, and one observation type of the UZF Package.", "optional": true, - "tagged": false + "tagged": false, + "arms": { + "cellid": { + "type": "array", + "tagged": false, + "dtype": "integer", + "index": true, + "cellid": true, + "shape": [ + "ncelldim" + ] + }, + "index": { + "type": "integer", + "tagged": false, + "index": true + }, + "boundname": { + "type": "string", + "tagged": false + } + } } } } diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml index 181d20ca..a4bf8174 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml @@ -36,18 +36,56 @@ description = "a string of characters used to identify the observation type." tagged = false [blocks.continuous.fields.continuous.item.fields.id] -type = "string" +type = "union" longname = "time" description = "Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be a boundary name. Otherwise ID is a cellid. If the model discretization is type DIS, cellid is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number)." tagged = false -[blocks.continuous.fields.continuous.item.fields.id2] +[blocks.continuous.fields.continuous.item.fields.id.arms.cellid] +type = "array" +tagged = false +dtype = "integer" +index = true +cellid = true +shape = [ + "ncelldim", +] + +[blocks.continuous.fields.continuous.item.fields.id.arms.index] +type = "integer" +tagged = false +index = true + +[blocks.continuous.fields.continuous.item.fields.id.arms.boundname] type = "string" +tagged = false + +[blocks.continuous.fields.continuous.item.fields.id2] +type = "union" longname = "time" description = "Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three observation types of the LAK Package, for two observation types of the MAW Package, and one observation type of the UZF Package." optional = true tagged = false +[blocks.continuous.fields.continuous.item.fields.id2.arms.cellid] +type = "array" +tagged = false +dtype = "integer" +index = true +cellid = true +shape = [ + "ncelldim", +] + +[blocks.continuous.fields.continuous.item.fields.id2.arms.index] +type = "integer" +tagged = false +index = true + +[blocks.continuous.fields.continuous.item.fields.id2.arms.boundname] +type = "string" +tagged = false + [blocks.continuous.header.field] name = "output" type = "record" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml index f14468c9..a0e515da 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml @@ -45,7 +45,7 @@ blocks: description: a string of characters used to identify the observation type. tagged: false id: - type: string + type: union longname: time description: Text identifying cell where observation is located. For packages other than NPF, if boundary names are defined in the corresponding package input file, ID can be @@ -53,8 +53,24 @@ blocks: is three integers (layer, row, column). If the discretization is DISV, cellid is two integers (layer, cell number). If the discretization is DISU, cellid is one integer (node number). tagged: false + arms: + cellid: + type: array + tagged: false + dtype: integer + index: true + cellid: true + shape: + - ncelldim + index: + type: integer + tagged: false + index: true + boundname: + type: string + tagged: false id2: - type: string + type: union longname: time description: Text identifying cell adjacent to cell identified by ID. The form of ID2 is as described for ID. ID2 is used for intercell-flow observations of a GWF model, for three @@ -62,6 +78,22 @@ blocks: one observation type of the UZF Package. optional: true tagged: false + arms: + cellid: + type: array + tagged: false + dtype: integer + index: true + cellid: true + shape: + - ncelldim + index: + type: integer + tagged: false + index: true + boundname: + type: string + tagged: false header: field: name: output diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 238b7443..4d4aba6c 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -369,6 +369,23 @@ def test_migrate_marks_cellids(dfn_dir, name, block, cols): assert (field.shape, field.index, field.cellid) == (["ncelldim"], True, True) +@pytest.mark.parametrize("col, optional", [("id", False), ("id2", True)]) +def test_migrate_obs_id_union(dfn_dir, col, optional): + """A v1 string with numeric_index (utl-obs's id/id2) becomes an untagged + union of a cellid, a 1-based index, and a boundname.""" + component = _migrate_dev3(dfn_dir, "utl-obs") + item = component.blocks["continuous"].fields["continuous"].item + field = item.fields[col] + assert isinstance(field, v2.Union) + assert (field.tagged, field.optional) == (False, optional) + cellid, index, boundname = field.arms.values() + assert isinstance(cellid, v2.Array) + assert (cellid.shape, cellid.index, cellid.cellid) == (["ncelldim"], True, True) + assert isinstance(index, v2.Integer) and index.index + assert isinstance(boundname, v2.String) + assert not any(arm.tagged for arm in field.arms.values()) + + def test_migrate_sfr_ic_is_signed_index(dfn_dir): component = _migrate_dev3(dfn_dir, "gwf-sfr") item = component.blocks["connectiondata"].fields["connectiondata"].item diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 778fb266..b3ce2a8f 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -529,6 +529,8 @@ Type `union`. Sum type. `{string: Scalar | Array | Record}`. Subfields, required. +A union is usually keyword-led: each arm begins with its own keyword, which picks the arm. An untagged union's arms begin with a value instead, and the value alone may not pick the arm. `utl-obs`'s `id` is a cellid, a 1-based index (e.g. a lake number), or a boundname, depending on the parent package and the observation type. As with a runtime-resolved [foreign key](#primary-and-foreign-keys), the codec resolves which arm applies. + #### List Type `list`. Collection type. Unlimited but for one rule: a list may not contain another list. Lists are distinct from arrays in two ways: a list element may be a composite type and a list admits sparse representations. diff --git a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py index 95102e04..c45965f4 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1697,6 +1697,34 @@ def _parse_shape(s: str) -> list[str]: result.append(f"{op}{parsed}") return result + def _to_id_union() -> v2.Union: + # A v1 string with `numeric_index` (only utl-obs's `id`/`id2`) + # identifies something in the parent: a cell, a 1-based number + # (e.g. a lake or reach), or a boundname. Which one depends on the + # parent and is resolved by the codec, like an `fk_ref` target. + return v2.Union( + name=_name, + longname=longname, + description=description, + optional=optional, + developmode=developmode, + removed=removed, + deprecated=deprecated, + tagged=False, + arms={ + "cellid": v2.Array( + name="cellid", + tagged=False, + dtype="integer", + shape=["ncelldim"], + index=True, + cellid=True, + ), + "index": v2.Integer(name="index", tagged=False, index=True), + "boundname": v2.String(name="boundname", tagged=False), + }, + ) + def _to_scalar() -> v2.Scalar: assert _type is not None if _type == "keyword": @@ -2035,6 +2063,8 @@ def _lookup(rname: str) -> dict | None: index=numeric_index if dtype == "integer" else False, ) + if _type == "string" and numeric_index: + return _to_id_union() return _to_scalar() blocks: dict[str, v2.Block] = {}