From 95f1d4c8249760a6c34427fa42bd430c87b2c803 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 09:51:03 -0800 Subject: [PATCH 01/11] feat(dfns): binding parameters A component declares what it needs from whatever binds its file, in `params`: an exchange's two models (from sim-nam's exchanges row), a mover's or GNC's (passed down from the exchange), SPC's and SPCA's flow package (from SSM's row), and OBS's target (its parent). An fk names one with `param`, a cellid's `model` names one instead of "1"/"2", and a `when` may test one (MVR's MODELNAMES is set exactly when model2 is). The rules exchange_models and ssm_package go; no_advanced_transport takes the part of ssm_package that isn't about the binding. OBS's obstype names one of its target's observation types, under the reserved key set name `observations`. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/v2.0.0.dev2/exg-chfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev2/exg-chfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev2/exg-gwegwe.json | 14 +- .../__snapshots__/v2.0.0.dev2/exg-gwegwe.toml | 12 +- .../__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml | 11 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.json | 14 +- .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml | 12 +- .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml | 11 +- .../__snapshots__/v2.0.0.dev2/exg-olfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev2/exg-olfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev2/gwf-gnc.json | 20 +- .../__snapshots__/v2.0.0.dev2/gwf-gnc.toml | 18 +- .../__snapshots__/v2.0.0.dev2/gwf-gnc.yaml | 15 +- .../__snapshots__/v2.0.0.dev2/gwf-mvr.json | 34 +- .../__snapshots__/v2.0.0.dev2/gwf-mvr.toml | 32 +- .../__snapshots__/v2.0.0.dev2/gwf-mvr.yaml | 26 +- .../__snapshots__/v2.0.0.dev2/utl-obs.json | 9 + .../__snapshots__/v2.0.0.dev2/utl-obs.toml | 7 + .../__snapshots__/v2.0.0.dev2/utl-obs.yaml | 6 + .../__snapshots__/v2.0.0.dev2/utl-spc.json | 9 +- .../__snapshots__/v2.0.0.dev2/utl-spc.toml | 7 +- .../__snapshots__/v2.0.0.dev2/utl-spc.yaml | 7 +- .../__snapshots__/v2.0.0.dev2/utl-spca.json | 10 + .../__snapshots__/v2.0.0.dev2/utl-spca.toml | 8 + .../__snapshots__/v2.0.0.dev2/utl-spca.yaml | 7 + .../__snapshots__/v2.0.0.dev3/exg-chfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev3/exg-chfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev3/exg-gwegwe.json | 14 +- .../__snapshots__/v2.0.0.dev3/exg-gwegwe.toml | 12 +- .../__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml | 11 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.json | 14 +- .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml | 12 +- .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml | 11 +- .../__snapshots__/v2.0.0.dev3/exg-olfgwf.json | 14 +- .../__snapshots__/v2.0.0.dev3/exg-olfgwf.toml | 12 +- .../__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml | 11 +- .../__snapshots__/v2.0.0.dev3/gwf-gnc.json | 20 +- .../__snapshots__/v2.0.0.dev3/gwf-gnc.toml | 18 +- .../__snapshots__/v2.0.0.dev3/gwf-gnc.yaml | 15 +- .../__snapshots__/v2.0.0.dev3/gwf-mvr.json | 34 +- .../__snapshots__/v2.0.0.dev3/gwf-mvr.toml | 32 +- .../__snapshots__/v2.0.0.dev3/gwf-mvr.yaml | 26 +- .../__snapshots__/v2.0.0.dev3/utl-obs.json | 15 +- .../__snapshots__/v2.0.0.dev3/utl-obs.toml | 11 + .../__snapshots__/v2.0.0.dev3/utl-obs.yaml | 9 + .../__snapshots__/v2.0.0.dev3/utl-spc.json | 9 +- .../__snapshots__/v2.0.0.dev3/utl-spc.toml | 7 +- .../__snapshots__/v2.0.0.dev3/utl-spc.yaml | 7 +- .../__snapshots__/v2.0.0.dev3/utl-spca.json | 10 + .../__snapshots__/v2.0.0.dev3/utl-spca.toml | 8 + .../__snapshots__/v2.0.0.dev3/utl-spca.yaml | 7 + autotest/dfns/test_migrate.py | 40 +- autotest/dfns/test_schema_relations.py | 269 ++++++++++++-- docs/md/dfnspec.md | 106 +++++- .../dfns/migrate_to_v2_0_0_dev2.py | 105 ++++-- .../dfns/migrate_to_v2_0_0_dev3.py | 21 +- modflow_devtools/dfns/schema.json | 125 ++++++- modflow_devtools/dfns/schema.py | 350 ++++++++++++++++-- 67 files changed, 1571 insertions(+), 258 deletions(-) diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json index 26d8a078..0e5864c1 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json @@ -76,14 +76,14 @@ "longname": "cellid of cell in surface water model", "description": "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of cell in groundwater model", "description": "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "bedleak": { "type": "double", @@ -105,5 +105,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml index 2240fce2..37e817e1 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml @@ -65,14 +65,14 @@ type = "cellid" longname = "cellid of cell in surface water model" description = "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of cell in groundwater model" description = "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.bedleak] type = "double" @@ -85,3 +85,11 @@ type = "double" longname = "factor used for conductance calculation" description = "is the factor used for the conductance calculation. The definition for this parameter depends the type of surface water model and whether or not the fixed_conductance option is specified. If the fixed_conductance option is specified, then the hydraulic conductance is calculated as the product of bedleak and cfact. In this case, the conductance is fixed and does not change as a function of the calculated surface water and groundwater head. If the fixed_conductance option is not specified, then the definition of cfact depends on whether the surface water model represents one-dimensional channel flow or two-dimensional overland flow. If the surface water model represents one-dimensional channel flow, then cfact is the length of the channel cell in the groundwater model cell. If the surface water model represents two-dimensional overland flow, then cfact is the intersection area of the overland flow cell and the underlying groundwater model cell." tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml index 9981c4da..7c653faf 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml @@ -71,7 +71,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of cell in groundwater model @@ -81,7 +81,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 bedleak: type: double longname: bed leakance @@ -106,3 +106,10 @@ blocks: tagged: false shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json index 0d4dbf2c..c469b65f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json @@ -150,14 +150,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -210,5 +210,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml index 76ecd1e6..63c7c0a1 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml @@ -137,14 +137,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -188,3 +188,11 @@ description = "name of the GWE Exchange cell. BOUNDNAME is an ASCII character v optional = true when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml index 343477c9..6191d249 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml @@ -151,7 +151,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -161,7 +161,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -211,3 +211,10 @@ blocks: tagged: false shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json index cf30b081..d00e4ae2 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json @@ -178,14 +178,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -238,5 +238,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml index 974ecfed..cab0a39e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml @@ -161,14 +161,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -212,3 +212,11 @@ description = "name of the GWF Exchange cell. BOUNDNAME is an ASCII character v optional = true when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml index 2cbb5a16..9ff4dafd 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml @@ -197,7 +197,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -207,7 +207,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -257,3 +257,10 @@ blocks: tagged: false shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json index 950a8f28..dcd93d18 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json @@ -150,14 +150,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -210,5 +210,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml index cfa5d7d7..485a1a32 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml @@ -137,14 +137,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -188,3 +188,11 @@ description = "name of the GWT Exchange cell. BOUNDNAME is an ASCII character v optional = true when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml index 37d00716..2dfa503e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml @@ -149,7 +149,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -159,7 +159,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -209,3 +209,10 @@ blocks: tagged: false shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json index 08e186fa..00f9fa80 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json @@ -76,14 +76,14 @@ "longname": "cellid of cell in surface water model", "description": "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of cell in groundwater model", "description": "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "bedleak": { "type": "double", @@ -105,5 +105,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml index f047cff7..48e2cca1 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml @@ -65,14 +65,14 @@ type = "cellid" longname = "cellid of cell in surface water model" description = "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of cell in groundwater model" description = "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.bedleak] type = "double" @@ -85,3 +85,11 @@ type = "double" longname = "factor used for conductance calculation" description = "is the factor used for the conductance calculation. The definition for this parameter depends the type of surface water model and whether or not the fixed_conductance option is specified. If the fixed_conductance option is specified, then the hydraulic conductance is calculated as the product of bedleak and cfact. In this case, the conductance is fixed and does not change as a function of the calculated surface water and groundwater head. If the fixed_conductance option is not specified, then the definition of cfact depends on whether the surface water model represents one-dimensional channel flow or two-dimensional overland flow. If the surface water model represents one-dimensional channel flow, then cfact is the length of the channel cell in the groundwater model cell. If the surface water model represents two-dimensional overland flow, then cfact is the intersection area of the overland flow cell and the underlying groundwater model cell." tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml index a7366ea7..b10cec16 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml @@ -71,7 +71,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of cell in groundwater model @@ -81,7 +81,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 bedleak: type: double longname: bed leakance @@ -106,3 +106,10 @@ blocks: tagged: false shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json index 8bff6db7..12569cf8 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json @@ -65,14 +65,14 @@ "longname": "GNC cellid n", "description": "is the cellid of the cell, $n$, in which the ghost node is located. For a structured grid that uses the DIS input file, CELLIDN is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDN is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm": { "type": "cellid", "longname": "GNC cellid n", "description": "is the cellid of the connecting cell, $m$, to which flow occurs from the ghost node. For a structured grid that uses the DIS input file, CELLIDM is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "cellidsj": { "type": "array", @@ -80,7 +80,7 @@ "description": "is the array of CELLIDS for the contributing j cells, which contribute to the interpolated head value at the ghost node. This item contains one CELLID for each of the contributing cells of the ghost node. Note that if the number of actual contributing cells needed by the user is less than NUMALPHAJ for any ghost node, then a dummy CELLID of zero(s) should be inserted with an associated contributing factor of zero. For a structured grid that uses the DIS input file, CELLID is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLID is the layer number and cell2d number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLID is the node number for the cell.", "tagged": false, "dtype": "cellid", - "model": "1", + "model": "model1", "allow_zero": true, "shape": [ "numalphaj" @@ -104,5 +104,19 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model1" + } + }, + "model2": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model2" + } + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml index 3d942779..c24b2bb0 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml @@ -56,14 +56,14 @@ type = "cellid" longname = "GNC cellid n" description = "is the cellid of the cell, $n$, in which the ghost node is located. For a structured grid that uses the DIS input file, CELLIDN is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDN is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.gncdata.fields.gncdata.item.fields.cellidm] type = "cellid" longname = "GNC cellid n" description = "is the cellid of the connecting cell, $m$, to which flow occurs from the ghost node. For a structured grid that uses the DIS input file, CELLIDM is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.gncdata.fields.gncdata.item.fields.cellidsj] type = "array" @@ -71,7 +71,7 @@ longname = "GNC contributing cells" description = "is the array of CELLIDS for the contributing j cells, which contribute to the interpolated head value at the ghost node. This item contains one CELLID for each of the contributing cells of the ghost node. Note that if the number of actual contributing cells needed by the user is less than NUMALPHAJ for any ghost node, then a dummy CELLID of zero(s) should be inserted with an associated contributing factor of zero. For a structured grid that uses the DIS input file, CELLID is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLID is the layer number and cell2d number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLID is the node number for the cell." tagged = false dtype = "cellid" -model = "1" +model = "model1" allow_zero = true shape = [ "numalphaj", @@ -86,3 +86,15 @@ dtype = "double" shape = [ "numalphaj", ] + +[params.model1] +type = "model" + +[params.model1.from] +exg-gwfgwf = "parent.model1" + +[params.model2] +type = "model" + +[params.model2.from] +exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml index ecf14697..d74df6f8 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml @@ -65,7 +65,7 @@ blocks: number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm: type: cellid longname: GNC cellid n @@ -75,7 +75,7 @@ blocks: layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell. tagged: false - model: '2' + model: model2 cellidsj: type: array longname: GNC contributing cells @@ -90,7 +90,7 @@ blocks: CELLID is the node number for the cell. tagged: false dtype: cellid - model: '1' + model: model1 allow_zero: true shape: - numalphaj @@ -109,3 +109,12 @@ blocks: - numalphaj shape: - numgnc +params: + model1: + type: model + from: + exg-gwfgwf: parent.model1 + model2: + type: model + from: + exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json index 9d865de7..63903b78 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json @@ -34,7 +34,7 @@ "type": "keyword", "longname": "precede all package names with model names", "description": "keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model.", - "optional": true + "when": "model2" }, "budget_filerecord": { "type": "record", @@ -102,8 +102,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname": { @@ -141,8 +143,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname1": { @@ -172,8 +176,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname2": { @@ -226,5 +232,19 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model1" + } + }, + "model2": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model2" + } + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml index 11ed5f52..b82e5273 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml @@ -29,7 +29,7 @@ optional = true type = "keyword" longname = "precede all package names with model names" description = "keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model." -optional = true +when = "model2" [blocks.options.fields.budget_filerecord] type = "record" @@ -90,8 +90,10 @@ when = "modelnames" tagged = false [blocks.packages.fields.packages.item.fields.mname.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.packages.fields.packages.item.fields.pname] type = "string" @@ -120,8 +122,10 @@ when = "modelnames" tagged = false [blocks.period.fields.stress_period_data.item.fields.mname1.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.period.fields.stress_period_data.item.fields.pname1] type = "string" @@ -151,8 +155,10 @@ when = "modelnames" tagged = false [blocks.period.fields.stress_period_data.item.fields.mname2.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.period.fields.stress_period_data.item.fields.pname2] type = "string" @@ -198,3 +204,15 @@ longname = "stress period number" description = "integer value specifying the starting stress period number for which the data specified in the PERIOD block apply. IPER must be less than or equal to NPER in the TDIS Package and greater than zero. The IPER value assigned to a stress period block must be greater than the IPER value assigned for the previous PERIOD block. The information specified in the PERIOD block will continue to apply for all subsequent stress periods, unless the program encounters another PERIOD block." tagged = false index = true + +[params.model1] +type = "model" + +[params.model1.from] +exg-gwfgwf = "parent.model1" + +[params.model2] +type = "model" + +[params.model2.from] +exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml index 62fe8f78..1a4b057a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml @@ -33,7 +33,7 @@ blocks: description: keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model. - optional: true + when: model2 budget_filerecord: type: record optional: true @@ -90,8 +90,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname: type: string description: is the name of a package that may be included in a subsequent stress period @@ -124,8 +125,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname1: type: string longname: provider package name @@ -158,8 +160,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname2: type: string longname: receiver package name @@ -211,3 +214,12 @@ blocks: another PERIOD block. tagged: false index: true +params: + model1: + type: model + from: + exg-gwfgwf: parent.model1 + model2: + type: model + from: + exg-gwfgwf: parent.model2 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 98328002..98224f56 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json @@ -109,5 +109,14 @@ } } } + }, + "params": { + "target": { + "from": "parent", + "type": [ + "model", + "package" + ] + } } } \ No newline at end of file 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 1e68cb00..44362e17 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml @@ -89,3 +89,10 @@ direction = "out" type = "keyword" description = "an optional keyword used to indicate that the output file should be written in binary (unformatted) form." optional = true + +[params.target] +from = "parent" +type = [ + "model", + "package", +] 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 eabdc90c..50e9c9db 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml @@ -104,3 +104,9 @@ blocks: description: an optional keyword used to indicate that the output file should be written in binary (unformatted) form. optional: true +params: + target: + from: parent + type: + - model + - package diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json index a888b61c..c1dfdbda 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json @@ -74,7 +74,7 @@ "pk": true, "fk": { "key": "bound", - "rule": "ssm_package" + "param": "target" } }, "spcsetting": { @@ -115,5 +115,12 @@ } } } + }, + "params": { + "target": { + "type": "package", + "from": "row.pname", + "rule": "no_advanced_transport" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml index 19694fc9..8174119f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml @@ -64,7 +64,7 @@ pk = true [blocks.period.fields.stress_period_data.item.fields.bndno.fk] key = "bound" -rule = "ssm_package" +param = "target" [blocks.period.fields.stress_period_data.item.fields.spcsetting] type = "union" @@ -93,3 +93,8 @@ longname = "stress period number" description = "integer value specifying the starting stress period number for which the data specified in the PERIOD block apply. IPER must be less than or equal to NPER in the TDIS Package and greater than zero. The IPER value assigned to a stress period block must be greater than the IPER value assigned for the previous PERIOD block. The information specified in the PERIOD block will continue to apply for all subsequent stress periods, unless the program encounters another PERIOD block." tagged = false index = true + +[params.target] +type = "package" +from = "row.pname" +rule = "no_advanced_transport" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml index 6401b1dc..b94b4b7b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml @@ -65,7 +65,7 @@ blocks: pk: true fk: key: bound - rule: ssm_package + param: target spcsetting: type: union description: 'line of information that is parsed into a keyword and values. Keyword values @@ -104,3 +104,8 @@ blocks: another PERIOD block. tagged: false index: true +params: + target: + type: package + from: row.pname + rule: no_advanced_transport diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json index fe85851a..914e8407 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json @@ -87,5 +87,15 @@ } } } + }, + "params": { + "target": { + "from": "row.pname", + "rule": "no_advanced_transport", + "type": [ + "gwf-rcha", + "gwf-evta" + ] + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml index ede376ee..829e5939 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml @@ -75,3 +75,11 @@ longname = "stress period number" description = "integer value specifying the starting stress period number for which the data specified in the PERIOD block apply. IPER must be less than or equal to NPER in the TDIS Package and greater than zero. The IPER value assigned to a stress period block must be greater than the IPER value assigned for the previous PERIOD block. The information specified in the PERIOD block will continue to apply for all subsequent stress periods, unless the program encounters another PERIOD block." tagged = false index = true + +[params.target] +from = "row.pname" +rule = "no_advanced_transport" +type = [ + "gwf-rcha", + "gwf-evta", +] diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml index fd07e6f1..84e77713 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml @@ -84,3 +84,10 @@ blocks: another PERIOD block. tagged: false index: true +params: + target: + from: row.pname + rule: no_advanced_transport + type: + - gwf-rcha + - gwf-evta diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json index 443459ad..f4989499 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json @@ -76,14 +76,14 @@ "longname": "cellid of cell in surface water model", "description": "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of cell in groundwater model", "description": "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "bedleak": { "type": "double", @@ -167,5 +167,15 @@ "nexg" ] } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml index de1fa892..2691cd05 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml @@ -65,14 +65,14 @@ type = "cellid" longname = "cellid of cell in surface water model" description = "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of cell in groundwater model" description = "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.bedleak] type = "double" @@ -145,3 +145,11 @@ dtype = "double" shape = [ "nexg", ] + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml index 7223260f..c549fb17 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml @@ -71,7 +71,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of cell in groundwater model @@ -81,7 +81,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 bedleak: type: double longname: bed leakance @@ -157,3 +157,10 @@ memory: dtype: double shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json index abe47cdf..582d68ea 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json @@ -153,14 +153,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -344,5 +344,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml index 70642d4b..64b4ed9d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml @@ -140,14 +140,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -316,3 +316,11 @@ index = true type = "string" when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml index afa9e9c7..fdc98b16 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml @@ -153,7 +153,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -163,7 +163,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -318,3 +318,10 @@ observations: type: string when: boundnames tagged: false +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json index ae1535f9..c17b70b3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json @@ -181,14 +181,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -372,5 +372,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml index 51620465..18d1ac65 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml @@ -164,14 +164,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -340,3 +340,11 @@ index = true type = "string" when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml index 8f0a0dab..4daf29e6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml @@ -199,7 +199,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -209,7 +209,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -364,3 +364,10 @@ observations: type: string when: boundnames tagged: false +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json index b9eab8a7..29dc60e6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json @@ -153,14 +153,14 @@ "longname": "cellid of first cell", "description": "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of second cell", "description": "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "ihc": { "type": "integer", @@ -344,5 +344,15 @@ } } } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml index 4e5d019e..f32e0976 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml @@ -140,14 +140,14 @@ type = "cellid" longname = "cellid of first cell" description = "is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of second cell" description = "is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.ihc] type = "integer" @@ -316,3 +316,11 @@ index = true type = "string" when = "boundnames" tagged = false + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml index 00421ac0..023714af 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml @@ -151,7 +151,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of second cell @@ -161,7 +161,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 ihc: type: integer longname: integer flag for connection type @@ -316,3 +316,10 @@ observations: type: string when: boundnames tagged: false +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json index df904839..d5de9451 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json @@ -76,14 +76,14 @@ "longname": "cellid of cell in surface water model", "description": "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm2": { "type": "cellid", "longname": "cellid of cell in groundwater model", "description": "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "bedleak": { "type": "double", @@ -167,5 +167,15 @@ "nexg" ] } + }, + "params": { + "model1": { + "type": "model", + "from": "row.exgmnamea" + }, + "model2": { + "type": "model", + "from": "row.exgmnameb" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml index c07fa562..b27e9e21 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml @@ -65,14 +65,14 @@ type = "cellid" longname = "cellid of cell in surface water model" description = "is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.exchangedata.fields.exchangedata.item.fields.cellidm2] type = "cellid" longname = "cellid of cell in groundwater model" description = "is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.exchangedata.fields.exchangedata.item.fields.bedleak] type = "double" @@ -145,3 +145,11 @@ dtype = "double" shape = [ "nexg", ] + +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml index 8f632722..c9a3f349 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml @@ -71,7 +71,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm2: type: cellid longname: cellid of cell in groundwater model @@ -81,7 +81,7 @@ blocks: number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. tagged: false - model: '2' + model: model2 bedleak: type: double longname: bed leakance @@ -157,3 +157,10 @@ memory: dtype: double shape: - nexg +params: + model1: + type: model + from: row.exgmnamea + model2: + type: model + from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json index e611512e..f0799018 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json @@ -65,14 +65,14 @@ "longname": "GNC cellid n", "description": "is the cellid of the cell, $n$, in which the ghost node is located. For a structured grid that uses the DIS input file, CELLIDN is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDN is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell.", "tagged": false, - "model": "1" + "model": "model1" }, "cellidm": { "type": "cellid", "longname": "GNC cellid n", "description": "is the cellid of the connecting cell, $m$, to which flow occurs from the ghost node. For a structured grid that uses the DIS input file, CELLIDM is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell.", "tagged": false, - "model": "2" + "model": "model2" }, "cellidsj": { "type": "array", @@ -80,7 +80,7 @@ "description": "is the array of CELLIDS for the contributing j cells, which contribute to the interpolated head value at the ghost node. This item contains one CELLID for each of the contributing cells of the ghost node. Note that if the number of actual contributing cells needed by the user is less than NUMALPHAJ for any ghost node, then a dummy CELLID of zero(s) should be inserted with an associated contributing factor of zero. For a structured grid that uses the DIS input file, CELLID is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLID is the layer number and cell2d number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLID is the node number for the cell.", "tagged": false, "dtype": "cellid", - "model": "1", + "model": "model1", "allow_zero": true, "shape": [ "numalphaj" @@ -116,5 +116,19 @@ "source": "numalphaj", "type": "integer" } + }, + "params": { + "model1": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model1" + } + }, + "model2": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model2" + } + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml index 09c72084..96cfaa0a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml @@ -56,14 +56,14 @@ type = "cellid" longname = "GNC cellid n" description = "is the cellid of the cell, $n$, in which the ghost node is located. For a structured grid that uses the DIS input file, CELLIDN is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDN is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell." tagged = false -model = "1" +model = "model1" [blocks.gncdata.fields.gncdata.item.fields.cellidm] type = "cellid" longname = "GNC cellid n" description = "is the cellid of the connecting cell, $m$, to which flow occurs from the ghost node. For a structured grid that uses the DIS input file, CELLIDM is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell." tagged = false -model = "2" +model = "model2" [blocks.gncdata.fields.gncdata.item.fields.cellidsj] type = "array" @@ -71,7 +71,7 @@ longname = "GNC contributing cells" description = "is the array of CELLIDS for the contributing j cells, which contribute to the interpolated head value at the ghost node. This item contains one CELLID for each of the contributing cells of the ghost node. Note that if the number of actual contributing cells needed by the user is less than NUMALPHAJ for any ghost node, then a dummy CELLID of zero(s) should be inserted with an associated contributing factor of zero. For a structured grid that uses the DIS input file, CELLID is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLID is the layer number and cell2d number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLID is the node number for the cell." tagged = false dtype = "cellid" -model = "1" +model = "model1" allow_zero = true shape = [ "numalphaj", @@ -96,3 +96,15 @@ type = "integer" set_in = "ar" source = "numalphaj" type = "integer" + +[params.model1] +type = "model" + +[params.model1.from] +exg-gwfgwf = "parent.model1" + +[params.model2] +type = "model" + +[params.model2.from] +exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml index bf7c0878..9df3f280 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml @@ -65,7 +65,7 @@ blocks: number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDN is the node number for the cell. tagged: false - model: '1' + model: model1 cellidm: type: cellid longname: GNC cellid n @@ -75,7 +75,7 @@ blocks: layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM is the node number for the cell. tagged: false - model: '2' + model: model2 cellidsj: type: array longname: GNC contributing cells @@ -90,7 +90,7 @@ blocks: CELLID is the node number for the cell. tagged: false dtype: cellid - model: '1' + model: model1 allow_zero: true shape: - numalphaj @@ -118,3 +118,12 @@ memory: set_in: ar source: numalphaj type: integer +params: + model1: + type: model + from: + exg-gwfgwf: parent.model1 + model2: + type: model + from: + exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json index 8c8080c9..06770e7e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json @@ -34,7 +34,7 @@ "type": "keyword", "longname": "precede all package names with model names", "description": "keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model.", - "optional": true + "when": "model2" }, "budget_filerecord": { "type": "record", @@ -102,8 +102,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname": { @@ -141,8 +143,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname1": { @@ -172,8 +176,10 @@ "when": "modelnames", "tagged": false, "fk": { - "path": "sim-nam.models.mname", - "rule": "exchange_models" + "param": [ + "model1", + "model2" + ] } }, "pname2": { @@ -238,5 +244,19 @@ "source": "maxpackages", "type": "integer" } + }, + "params": { + "model1": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model1" + } + }, + "model2": { + "type": "model", + "from": { + "exg-gwfgwf": "parent.model2" + } + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml index 3e4ae664..007552f6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml @@ -29,7 +29,7 @@ optional = true type = "keyword" longname = "precede all package names with model names" description = "keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model." -optional = true +when = "model2" [blocks.options.fields.budget_filerecord] type = "record" @@ -90,8 +90,10 @@ when = "modelnames" tagged = false [blocks.packages.fields.packages.item.fields.mname.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.packages.fields.packages.item.fields.pname] type = "string" @@ -120,8 +122,10 @@ when = "modelnames" tagged = false [blocks.period.fields.stress_period_data.item.fields.mname1.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.period.fields.stress_period_data.item.fields.pname1] type = "string" @@ -151,8 +155,10 @@ when = "modelnames" tagged = false [blocks.period.fields.stress_period_data.item.fields.mname2.fk] -path = "sim-nam.models.mname" -rule = "exchange_models" +param = [ + "model1", + "model2", +] [blocks.period.fields.stress_period_data.item.fields.pname2] type = "string" @@ -208,3 +214,15 @@ type = "integer" set_in = "ar" source = "maxpackages" type = "integer" + +[params.model1] +type = "model" + +[params.model1.from] +exg-gwfgwf = "parent.model1" + +[params.model2] +type = "model" + +[params.model2.from] +exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml index 5077a720..e06810b3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml @@ -33,7 +33,7 @@ blocks: description: keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model. - optional: true + when: model2 budget_filerecord: type: record optional: true @@ -90,8 +90,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname: type: string description: is the name of a package that may be included in a subsequent stress period @@ -124,8 +125,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname1: type: string longname: provider package name @@ -158,8 +160,9 @@ blocks: when: modelnames tagged: false fk: - path: sim-nam.models.mname - rule: exchange_models + param: + - model1 + - model2 pname2: type: string longname: receiver package name @@ -220,3 +223,12 @@ memory: set_in: ar source: maxpackages type: integer +params: + model1: + type: model + from: + exg-gwfgwf: parent.model1 + model2: + type: model + from: + exg-gwfgwf: parent.model2 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 dd61d70a..5f903e8c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json @@ -42,7 +42,11 @@ "type": "string", "longname": "observation type", "description": "a string of characters used to identify the observation type.", - "tagged": false + "tagged": false, + "fk": { + "key": "observations", + "param": "target" + } }, "id": { "type": "union", @@ -109,5 +113,14 @@ } } } + }, + "params": { + "target": { + "from": "parent", + "type": [ + "model", + "package" + ] + } } } \ No newline at end of file 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 b4f307bd..717328ff 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml @@ -38,6 +38,10 @@ longname = "observation type" description = "a string of characters used to identify the observation type." tagged = false +[blocks.continuous.fields.continuous.item.fields.obstype.fk] +key = "observations" +param = "target" + [blocks.continuous.fields.continuous.item.fields.id] type = "union" longname = "time" @@ -89,3 +93,10 @@ direction = "out" type = "keyword" description = "an optional keyword used to indicate that the output file should be written in binary (unformatted) form." optional = true + +[params.target] +from = "parent" +type = [ + "model", + "package", +] 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 051b1b8b..0fe88998 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml @@ -46,6 +46,9 @@ blocks: longname: observation type description: a string of characters used to identify the observation type. tagged: false + fk: + key: observations + param: target id: type: union longname: time @@ -104,3 +107,9 @@ blocks: description: an optional keyword used to indicate that the output file should be written in binary (unformatted) form. optional: true +params: + target: + from: parent + type: + - model + - package diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json index da8958ed..31cbc966 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json @@ -74,7 +74,7 @@ "pk": true, "fk": { "key": "bound", - "rule": "ssm_package" + "param": "target" } }, "spcsetting": { @@ -122,5 +122,12 @@ "source": "maxbound", "type": "integer" } + }, + "params": { + "target": { + "type": "package", + "from": "row.pname", + "rule": "no_advanced_transport" + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml index 2c6689a3..23fcf686 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml @@ -64,7 +64,7 @@ pk = true [blocks.period.fields.stress_period_data.item.fields.bndno.fk] key = "bound" -rule = "ssm_package" +param = "target" [blocks.period.fields.stress_period_data.item.fields.spcsetting] type = "union" @@ -98,3 +98,8 @@ index = true set_in = "ar" source = "maxbound" type = "integer" + +[params.target] +type = "package" +from = "row.pname" +rule = "no_advanced_transport" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml index 03bda0ae..abaade19 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml @@ -65,7 +65,7 @@ blocks: pk: true fk: key: bound - rule: ssm_package + param: target spcsetting: type: union description: 'line of information that is parsed into a keyword and values. Keyword values @@ -109,3 +109,8 @@ memory: set_in: ar source: maxbound type: integer +params: + target: + type: package + from: row.pname + rule: no_advanced_transport diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json index 7a5611d8..6c7ed342 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json @@ -87,5 +87,15 @@ } } } + }, + "params": { + "target": { + "from": "row.pname", + "rule": "no_advanced_transport", + "type": [ + "gwf-rcha", + "gwf-evta" + ] + } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml index c54b5567..8191e608 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml @@ -75,3 +75,11 @@ longname = "stress period number" description = "integer value specifying the starting stress period number for which the data specified in the PERIOD block apply. IPER must be less than or equal to NPER in the TDIS Package and greater than zero. The IPER value assigned to a stress period block must be greater than the IPER value assigned for the previous PERIOD block. The information specified in the PERIOD block will continue to apply for all subsequent stress periods, unless the program encounters another PERIOD block." tagged = false index = true + +[params.target] +from = "row.pname" +rule = "no_advanced_transport" +type = [ + "gwf-rcha", + "gwf-evta", +] diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml index 94d46c0c..52ed98c7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml @@ -84,3 +84,10 @@ blocks: another PERIOD block. tagged: false index: true +params: + target: + from: row.pname + rule: no_advanced_transport + type: + - gwf-rcha + - gwf-evta diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 195a1f61..1297b571 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -381,9 +381,17 @@ def test_migrate_mvr_keys(dfn_dir): blocks = component.blocks packages = blocks["packages"].fields["packages"].item.fields period = blocks["period"].fields["stress_period_data"].item.fields - # a model, and one of the exchange's two (MF6's xmvr_check_packages) - model = v2.ForeignKey(path="sim-nam.models.mname", rule="exchange_models") + # one of the exchange's two models (MF6's xmvr_check_packages), passed + # down from the exchange; unset under a model + model = v2.ForeignKey(param=("model1", "model2")) assert packages["mname"].fk == model + assert component.params == { + f"model{i}": v2.Param(type="model", from_={"exg-gwfgwf": f"parent.model{i}"}) + for i in (1, 2) + } + # MODELNAMES is set exactly when an exchange binds the mover + modelnames = blocks["options"].fields["modelnames"] + assert (modelnames.when, modelnames.optional) == ("model2", False) assert packages["pname"].pk == v2.PrimaryKey(scope="mname") # a package of that model, with its MOVER option set (MF6's check_packages) assert packages["pname"].fk == v2.ForeignKey( @@ -511,10 +519,14 @@ def test_migrate_maw_without_angledata(dfn_dir, tmp_path): def test_migrate_spc_bndno(dfn_dir): """SPC's bndno is one row per bound, and names a bound of the flow package its SSM row names.""" - blocks = _migrate_dev3(dfn_dir, "utl-spc").blocks + component = _migrate_dev3(dfn_dir, "utl-spc") + blocks = component.blocks bndno = blocks["period"].fields["stress_period_data"].item.fields["bndno"] assert bndno.pk == v2.PrimaryKey() - assert bndno.fk == v2.ForeignKey(key="bound", rule="ssm_package") + assert bndno.fk == v2.ForeignKey(key="bound", param="target") + assert component.params == { + "target": v2.Param(type="package", from_="row.pname", rule="no_advanced_transport") + } assert _fix_spc_relations("utl-spc", blocks) == blocks @@ -589,7 +601,7 @@ def test_migrate_gnc_cellids(dfn_dir): item = component.blocks["gncdata"].fields["gncdata"].item # Under an exchange, `cellidn` and `cellidsj` are in model 1's grid and # `cellidm` in model 2's (MF6's GhostNode.f90). - for name, model in [("cellidn", "1"), ("cellidm", "2")]: + for name, model in [("cellidn", "model1"), ("cellidm", "model2")]: field = item.fields[name] assert isinstance(field, v2.CellID) assert (field.model, field.allow_zero) == (model, False) @@ -599,7 +611,7 @@ def test_migrate_gnc_cellids(dfn_dir): assert (cellidsj.dtype, cellidsj.shape, cellidsj.model, cellidsj.allow_zero) == ( "cellid", ["numalphaj"], - "1", + "model1", True, ) assert cellidsj.render(inline=True) == "" @@ -663,7 +675,7 @@ def test_migrate_exchange_cellids(dfn_dir, name): component = _migrate_dev3(dfn_dir, name) assert component.subtype == "exchange" item = component.blocks["exchangedata"].fields["exchangedata"].item - for col, model in (("cellidm1", "1"), ("cellidm2", "2")): + for col, model in (("cellidm1", "model1"), ("cellidm2", "model2")): field = item.fields[col] assert isinstance(field, v2.CellID) assert (field.model, field.allow_zero) == (model, False) @@ -827,14 +839,18 @@ def test_migrate_period_header_is_index(dev3): def test_migrate_cellid_models_round_trip(dev3): - # "1"/"2" stay strings on disk in every format, and load back as such. + # A cellid's model names a parameter, on disk in every format. out, fmt = dev3 raw = _load(out / f"exg-gwfgwf.{fmt}", fmt) cols = raw["blocks"]["exchangedata"]["fields"]["exchangedata"]["item"]["fields"] - assert (cols["cellidm1"]["model"], cols["cellidm2"]["model"]) == ("1", "2") + assert (cols["cellidm1"]["model"], cols["cellidm2"]["model"]) == ("model1", "model2") dfns = v2.Dfns.load(out) gnc = dfns.components["gwf-gnc"].blocks["gncdata"].fields["gncdata"].item.fields - assert [gnc[c].model for c in ("cellidn", "cellidm", "cellidsj")] == ["1", "2", "1"] + assert [gnc[c].model for c in ("cellidn", "cellidm", "cellidsj")] == [ + "model1", + "model2", + "model1", + ] assert [type(gnc[c]) for c in ("cellidn", "cellidsj")] == [v2.CellID, v2.Array] assert gnc["cellidsj"].allow_zero @@ -1035,7 +1051,9 @@ def test_dfns_validate_obs_when_names_no_keyword_option(): cellid = v2.CellID(name="cellid", tagged=False) observation = v2.Union(name="wel", arms={"cellid": cellid, "boundname": boundname}) package = v2.Package(name="gwf-wel", observations={"wel": observation}) - with pytest.raises(ValueError, match="when='boundnames' names no keyword option of gwf-wel"): + with pytest.raises( + ValueError, match="when='boundnames' names no keyword option or parameter of gwf-wel" + ): v2.Dfns(components={"gwf-wel": package}) diff --git a/autotest/dfns/test_schema_relations.py b/autotest/dfns/test_schema_relations.py index 42e096de..bdb60430 100644 --- a/autotest/dfns/test_schema_relations.py +++ b/autotest/dfns/test_schema_relations.py @@ -17,6 +17,7 @@ List, Model, Package, + Param, PrimaryKey, Record, Simulation, @@ -442,11 +443,18 @@ def test_array_fk_requires_integer_or_string_dtype(dtype): Array(name="slnmnames", dtype=dtype, fk="models.mname") +# A GNC's or an exchange's two models, as parameters (see `Param`). +_MODELS = { + "model1": Param(type="model", from_="row.exgmnamea"), + "model2": Param(type="model", from_="row.exgmnameb"), +} + + def _cellid_ctx(field, name="gwf-gnc", parent="gwf-nam", dims=None): item = Record(name="item", fields={field.name: field}) lst = List(name="data", item=item) block = Block(name="data", fields={"data": lst}) - pkg = Package(name=name, parent=parent, blocks={"data": block}, dims=dims) + pkg = Package(name=name, parent=parent, blocks={"data": block}, dims=dims, params=_MODELS) gwf = Model(name="gwf-nam", blocks=None) return {"gwf-nam": gwf, name: pkg} @@ -476,8 +484,8 @@ def test_dfns_validate_cellid_array_shape_resolves(): def _exchange_ctx(*fields): item = Record(name="item", fields={f.name: f for f in fields}) block = Block(name="data", fields={"data": List(name="data", item=item)}) - exg = Package(name="exg-gwfgwf", subtype="exchange", blocks={"data": block}) - return {"exg-gwfgwf": exg} + exg = Package(name="exg-gwfgwf", subtype="exchange", blocks={"data": block}, params=_MODELS) + return {"gwf-nam": Model(name="gwf-nam", blocks=None), "exg-gwfgwf": exg} def _cellid_col(name, model=None, shape=None): @@ -487,10 +495,11 @@ def _cellid_col(name, model=None, shape=None): def test_dfns_validate_cellid_in_exchange(): - fields = (_cellid_col("cellidm1", "1"), _cellid_col("cellidm2", "2")) + fields = (_cellid_col("cellidm1", "model1"), _cellid_col("cellidm2", "model2")) spec = Dfns(components=_exchange_ctx(*fields)) item = spec.components["exg-gwfgwf"].blocks["data"].fields["data"].item - assert {n: f.model for n, f in item.fields.items()} == {"cellidm1": "1", "cellidm2": "2"} + models = {n: f.model for n, f in item.fields.items()} + assert models == {"cellidm1": "model1", "cellidm2": "model2"} @pytest.mark.parametrize("shape", [None, ["n"]]) @@ -501,11 +510,11 @@ def test_dfns_validate_exchange_cellid_must_name_model(shape): def test_dfns_validate_cellid_models_under_a_model(): - # GNC under a model: both models are that model, so "1"/"2" are valid. + # GNC under a model: its model parameters are unset, meaning that model. fields = ( - _cellid_col("cellidn", "1"), - _cellid_col("cellidm", "2"), - _cellid_col("cellidsj", "1", shape=("n",)), + _cellid_col("cellidn", "model1"), + _cellid_col("cellidm", "model2"), + _cellid_col("cellidsj", "model1", shape=("n",)), ) item = Record(name="item", fields={f.name: f for f in fields}) block = Block(name="data", fields={"data": List(name="data", item=item)}) @@ -514,16 +523,17 @@ def test_dfns_validate_cellid_models_under_a_model(): parent="gwf-nam", blocks={"data": block}, dims={"n": InputDim(value="2", scope="model")}, + params=_MODELS, ) spec = Dfns(components={"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-gnc": pkg}) assert "gwf-gnc" in spec.components def test_dfns_validate_cellid_no_mixing(): - item = Record(name="item", fields={"a": _cellid_col("a", "1"), "b": _cellid_col("b")}) + item = Record(name="item", fields={"a": _cellid_col("a", "model1"), "b": _cellid_col("b")}) block = Block(name="data", fields={"data": List(name="data", item=item)}) - pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}) - with pytest.raises(ValueError, match="mix model=None with '1'/'2'"): + pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}, params=_MODELS) + with pytest.raises(ValueError, match="mix model=None with a named model"): Dfns(components={"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-gnc": pkg}) @@ -538,17 +548,17 @@ def _union_cellid_pkg(arm_model, sibling_model): ) item = Record(name="item", fields={"a": _cellid_col("a", sibling_model), "id": union}) block = Block(name="data", fields={"data": List(name="data", item=item)}) - pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}) + pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}, params=_MODELS) return {"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-gnc": pkg} def test_dfns_validate_cellid_no_mixing_across_union_arm(): # A union arm's cellid is read in the same row as its sibling columns. - with pytest.raises(ValueError, match="mix model=None with '1'/'2'"): - Dfns(components=_union_cellid_pkg(None, "1")) + with pytest.raises(ValueError, match="mix model=None with a named model"): + Dfns(components=_union_cellid_pkg(None, "model1")) -@pytest.mark.parametrize("model", [None, "1"]) +@pytest.mark.parametrize("model", [None, "model1"]) def test_dfns_validate_cellid_union_arm_consistent(model): assert "gwf-gnc" in Dfns(components=_union_cellid_pkg(model, model)).components @@ -558,22 +568,33 @@ def test_dfns_validate_cellid_union_arms_are_alternatives(): # is checked against the row's other cellid columns. union = Union( name="id", - arms={"x": _cellid_col("x", "1"), "y": _cellid_col("y", "2")}, + arms={"x": _cellid_col("x", "model1"), "y": _cellid_col("y", "model2")}, ) - item = Record(name="item", fields={"a": _cellid_col("a", "1"), "id": union}) + item = Record(name="item", fields={"a": _cellid_col("a", "model1"), "id": union}) block = Block(name="data", fields={"data": List(name="data", item=item)}) - pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}) + pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}, params=_MODELS) Dfns(components={"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-gnc": pkg}) -@pytest.mark.parametrize("value", [True, 1, 2, 0, "true", "3", "a"]) +@pytest.mark.parametrize("value", [True, 1, 2, 0, "1", "", "model 1"]) @pytest.mark.parametrize("shape", [None, ["n"]]) def test_cellid_rejects_non_model_values(value, shape): - # `True == 1`: an int or bool must never be read as (or coerced to) a model. + # A model is a parameter's name. `True == 1`: an int or bool must never be + # read as (or coerced to) one. with pytest.raises(ValueError, match="model"): _cellid_col("cellid", value, shape=shape) +@pytest.mark.parametrize("params", [None, {"model1": Param(type="package", from_="parent")}]) +def test_dfns_validate_cellid_model_names_a_model_param(params): + # The name must be a parameter of the component, and of a model. + item = Record(name="item", fields={"a": _cellid_col("a", "model1")}) + block = Block(name="data", fields={"data": List(name="data", item=item)}) + pkg = Package(name="gwf-gnc", parent="gwf-nam", blocks={"data": block}, params=params) + with pytest.raises(ValueError, match="model='model1' names no parameter of gwf-gnc"): + Dfns(components={"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-gnc": pkg}) + + def test_cellid_render(): assert CellID(name="cellid", tagged=False).render(inline=True) == "" assert CellID(name="cellid", optional=True).render(inline=True) == "[CELLID ]" @@ -583,7 +604,7 @@ def test_cellid_render(): @pytest.mark.parametrize("attr", ["model", "allow_zero"]) def test_array_cellid_attrs_require_cellid_dtype(attr): - value = "1" if attr == "model" else True + value = "model1" if attr == "model" else True with pytest.raises(ValueError, match=f"{attr} requires dtype='cellid'"): Array(name="alphasj", dtype="integer", shape=["n"], **{attr: value}) @@ -848,7 +869,9 @@ def test_dfns_validate_when_names_keyword_option(): "option", [None, Keyword(name="other", optional=True), Integer(name="surf", optional=True)] ) def test_dfns_validate_when_names_no_keyword_option(option): - with pytest.raises(ValueError, match="when='surf' names no keyword option of gwf-evt"): + with pytest.raises( + ValueError, match="when='surf' names no keyword option or parameter of gwf-evt" + ): Dfns(components=_when_ctx("surf", option)) @@ -880,7 +903,12 @@ def test_integer_allow_zero_round_trips(): {"scope": "pname1", "key": "mover_provider"}, ForeignKey(scope="pname1", key="mover_provider"), ), - ({"key": "bound", "rule": "ssm_package"}, ForeignKey(key="bound", rule="ssm_package")), + ({"key": "bound", "param": "target"}, ForeignKey(key="bound", param="target")), + ({"param": ["model1", "model2"]}, ForeignKey(param=("model1", "model2"))), + ( + {"key": "observations", "param": "target"}, + ForeignKey(key="observations", param="target"), + ), ( {"path": "gwf-nam.packages.pname", "rule": "flow_model"}, ForeignKey(path="gwf-nam.packages.pname", rule="flow_model"), @@ -905,9 +933,17 @@ def test_key_fk_parts(): @pytest.mark.parametrize( "kwargs, match", [ - ({}, "exactly one of path and key"), - ({"path": "a.b", "key": "k", "scope": "s"}, "exactly one of path and key"), - ({"key": "mover_provider"}, "needs a scope or rule"), + ({}, "needs one of path, key and param"), + ({"path": "a.b", "key": "k", "scope": "s"}, "with a path can't have a key or param"), + ({"path": "a.b", "param": "target"}, "with a path can't have a key or param"), + ({"key": "mover_provider"}, "needs a scope, param or rule"), + ({"key": "bound", "scope": "s", "param": "p"}, "one of scope and param, not both"), + ({"key": "bound", "param": ["p", "q"]}, "picks its instance by one param"), + ({"param": "p", "scope": "s"}, "only a param can't have a scope or rule"), + ({"param": "p", "rule": "flow_model"}, "only a param can't have a scope or rule"), + ({"param": []}, "at least one parameter"), + ({"param": ["p", "p"]}, "repeat a parameter"), + ({"param": "a.b"}, "must be a parameter name"), ({"key": "a.b", "scope": "s"}, "must be a key set name"), ({"path": "a.b", "rule": "nope"}, "fk rule 'nope' is not one of"), ], @@ -1062,6 +1098,7 @@ def _nam(name, ftype): parent="sim-nam", subtype="exchange", ftype="GWF6-GWF6", + params=_MODELS, blocks={"options": Block(name="options", fields={"mvr_filerecord": mvr_file})}, ) return {"sim-nam": sim, "gwf-nam": _nam("gwf-nam", "GWF6"), "exg-gwfgwf": exg}, _nam @@ -1107,7 +1144,13 @@ def _mover_pkgs(lak_keys=None, wel_keys=None, wel_options=("mover",)): return {"gwf-lak": lak, "gwf-wel": wel} -_MNAME_FK = {"path": "sim-nam.models.mname", "rule": "exchange_models"} +_MNAME_FK = {"param": ["model1", "model2"]} + +# A mover's models, passed down from the exchange that binds it. +_FORWARD = { + "model1": Param(type="model", from_={"exg-gwfgwf": "parent.model1"}), + "model2": Param(type="model", from_={"exg-gwfgwf": "parent.model2"}), +} _PNAME_FK = {"path": "gwf-nam.packages.pname", "scope": "mname", "rule": "mover_enabled"} @@ -1152,6 +1195,7 @@ def _ctx_mvr(mname_fk=_MNAME_FK, id1_fk=None, id2_fk=None, pname_fk=_PNAME_FK, * parent=["exg-gwfgwf", "gwf-nam"], ftype="MVR6", blocks={"options": _options("modelnames"), "packages": packages, "period": period}, + params=_FORWARD, ) return {**_ctx_sim()[0], "gwf-mvr": mvr, **(pkgs or _mover_pkgs())} @@ -1160,7 +1204,7 @@ def test_dfns_validate_mover_keys(): spec = Dfns(components=_ctx_mvr()) period = spec.components["gwf-mvr"].blocks["period"].fields["stress_period_data"] assert period.item.fields["id1"].fk == ForeignKey(scope="pname1", key="mover_provider") - assert period.item.fields["mname1"].fk.rule == "exchange_models" + assert period.item.fields["mname1"].fk.params == ("model1", "model2") assert spec.components["gwf-wel"].keys["mover_provider"].bound == "maxbound" @@ -1181,10 +1225,9 @@ def test_dfns_validate_rule_with_scope_checks_scope(): def test_dfns_validate_rule_fk_path_checked(): - with pytest.raises(ValueError, match="'nope' is not the pk of list 'models'"): - Dfns( - components=_ctx_mvr(mname_fk={"path": "sim-nam.models.nope", "rule": "exchange_models"}) - ) + bad = {**_PNAME_FK, "path": "gwf-nam.packages.nope"} + with pytest.raises(ValueError, match="'nope' is not the pk of list 'packages'"): + Dfns(components=_ctx_mvr(pname_fk=bad)) def test_dfns_validate_key_undeclared(): @@ -1284,6 +1327,7 @@ def _ctx_transport(spc_key="bound", ssm_keys=_SOURCE, srctype=None): parent="gwt-ssm", subtype="utility", ftype="SPC6", + params={"target": Param(type="package", from_="row.pname", rule="no_advanced_transport")}, blocks={ "period": _list_block( "period", @@ -1292,7 +1336,7 @@ def _ctx_transport(spc_key="bound", ssm_keys=_SOURCE, srctype=None): tagged=False, index=True, pk=True, - fk={"key": spc_key, "rule": "ssm_package"}, + fk={"key": spc_key, "param": "target"}, ), Double(name="concentration", tagged=False), list_name="stress_period_data", @@ -1312,7 +1356,160 @@ def test_dfns_validate_ssm_and_spc_rules(): item = spec.components["gwt-ssm"].blocks["fileinput"].fields["fileinput"].item assert item.fields["pname"].fk.rule == "flow_model" period = spec.components["utl-spc"].blocks["period"].fields["stress_period_data"] - assert period.item.fields["bndno"].fk == ForeignKey(key="bound", rule="ssm_package") + assert period.item.fields["bndno"].fk == ForeignKey(key="bound", param="target") + + +# Binding parameters (`params`, see `Param`): what a component needs from +# whatever binds it, named by an fk's `param`, a cellid's `model` or a `when`. + + +def _with_params(ctx, name, **params): + """`ctx` with component `name`'s parameters replaced or added.""" + component = ctx[name] + merged = {**(component.params or {}), **params} + return {**ctx, name: component.model_copy(update={"params": merged})} + + +def test_param_loads_and_dumps(): + toml = {"type": ["gwf-rcha", "gwf-evta"], "from": "row.pname", "rule": "no_advanced_transport"} + pkg = Package.model_validate({"name": "utl-spca", "params": {"target": toml}}) + assert pkg.params["target"] == Param( + type=["gwf-rcha", "gwf-evta"], from_="row.pname", rule="no_advanced_transport" + ) + assert pkg.model_dump(exclude_none=True)["params"] == {"target": toml} + forward = {"type": "model", "from": {"exg-gwfgwf": "parent.model1"}} + assert Param.model_validate(forward).source_for("exg-gwfgwf") == "parent.model1" + assert Param.model_validate(forward).source_for("gwf-nam") is None + + +@pytest.mark.parametrize( + "source, match", + [ + ("row", "must be 'parent', 'parent.' or 'row.'"), + ("row.a.b", "must be 'parent', 'parent.' or 'row.'"), + ("parent.", "must be 'parent', 'parent.' or 'row.'"), + ("sibling.x", "must be 'parent', 'parent.' or 'row.'"), + ({}, "needs at least one binder"), + ({"exg-gwfgwf": "nope"}, "must be 'parent'"), + ], +) +def test_param_source_rejected(source, match): + with pytest.raises(ValueError, match=match): + Param(type="model", from_=source) + + +def test_param_rule_checked(): + with pytest.raises(ValueError, match="parameter rule 'nope' is not one of"): + Param(type="package", from_="parent", rule="nope") + + +def test_param_name_checked(): + with pytest.raises(ValueError, match="parameter name 'a-b' must be an identifier"): + Package(name="x", params={"a-b": Param(type="model", from_="parent")}) + + +def test_dfns_validate_params(): + """An exchange's models come from its binding row, a mover's from the + exchange, and SPC's target from SSM's row.""" + spec = Dfns(components=_ctx_transport()) + assert spec.components["exg-gwfgwf"].params == _MODELS + assert spec.components["gwf-mvr"].params["model1"].source_for("gwf-nam") is None + + +@pytest.mark.parametrize( + "name, param, match", + [ + ( + "exg-gwfgwf", + Param(type="model", from_="row.nope"), + "under sim-nam: the binding row has no column 'nope'", + ), + ( + "exg-gwfgwf", + Param(type="model", from_="row.exgtype"), + "the binding row's 'exgtype' has no fk to follow", + ), + ( + "gwf-mvr", + Param(type="model", from_={"exg-gwfgwf": "parent.model3"}), + "exg-gwfgwf declares no parameter 'model3'", + ), + ( + "gwf-mvr", + Param(type="model", from_={"exg-gwtgwt": "parent.model1"}), + r"\['exg-gwtgwt'\] don't bind gwf-mvr", + ), + ( + "gwf-mvr", + Param(type="model", from_="parent"), + "under exg-gwfgwf: the binder isn't of type 'model'", + ), + ( + "gwf-mvr", + Param(type="gwf-wel", from_={"exg-gwfgwf": "parent.model1"}), + "exg-gwfgwf's 'model1' names no 'gwf-wel'", + ), + ( + "exg-gwfgwf", + Param(type="package", from_="row.exgmnamea"), + "'exgmnamea' names nothing of type 'package'", + ), + ("exg-gwfgwf", Param(type="nope-nam", from_="row.exgmnamea"), "admits no component"), + ], +) +def test_dfns_validate_params_bad(name, param, match): + with pytest.raises(ValueError, match=match): + Dfns(components=_with_params(_ctx_transport(), name, model1=param)) + + +def test_dfns_validate_fk_param_undeclared(): + ctx = _ctx_transport() + ctx["gwf-mvr"] = ctx["gwf-mvr"].model_copy(update={"params": None}) + with pytest.raises(ValueError, match="fk param='model1': gwf-mvr declares no such parameter"): + Dfns(components=ctx) + + +def test_dfns_validate_fk_param_only_names_from_a_string(): + with pytest.raises(ValueError, match="only a string column may name a parameter's instance"): + Dfns(components=_ctx_mvr(id1_fk={"param": "model1"})) + + +def test_dfns_validate_observations_key_needs_a_string(): + ctx = _ctx_transport(spc_key="observations") + with pytest.raises(ValueError, match="observation types are named by a string column"): + Dfns(components=ctx) + + +def test_keyset_observations_reserved(): + with pytest.raises(ValueError, match="'observations' is reserved"): + Package(name="x", keys={"observations": "packagedata.ifno"}) + + +def _mvr_with_modelnames(**attrs): + ctx = _ctx_mvr() + mvr = ctx["gwf-mvr"] + options = Block(name="options", fields={"modelnames": Keyword(name="modelnames", **attrs)}) + ctx["gwf-mvr"] = mvr.model_copy(update={"blocks": {**mvr.blocks, "options": options}}) + return ctx + + +def test_dfns_validate_when_names_param(): + """`MODELNAMES` is set exactly when the mover's `model2` is: when an + exchange binds it.""" + spec = Dfns(components=_mvr_with_modelnames(when="model2")) + assert spec.components["gwf-mvr"].blocks["options"].fields["modelnames"].when == "model2" + + +@pytest.mark.parametrize( + "when, match", + [ + ("model3", "when='model3' names no keyword option or parameter of gwf-mvr"), + ("modelnames", "when='modelnames' names the field itself"), + ], +) +def test_dfns_validate_when_names_param_bad(when, match): + with pytest.raises(ValueError, match=match): + Dfns(components=_mvr_with_modelnames(when=when)) def test_dfns_validate_multi_path_keyset(): @@ -1340,5 +1537,5 @@ def test_dfns_validate_multi_path_keyset_types_differ(): def test_dfns_validate_rule_key_undeclared(): - with pytest.raises(ValueError, match="no component in the spec declares key 'nope'"): + with pytest.raises(ValueError, match="no component the parameter 'target' can name declares"): Dfns(components=_ctx_transport(spc_key="nope")) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 9bc94909..c6aca01b 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -15,6 +15,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [`memory`](#memory) - [`observations`](#observations) - [`keys`](#keys) + - [`params`](#params) - [Component types](#component-types) - [Simulation](#simulation) - [Model](#model) @@ -96,6 +97,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [Primary/foreign keys](#primary-and-foreign-keys) - [Scoped keys](#scoped-keys) - [Key sets](#key-sets) + - [Parameters](#parameters) - [Rules](#rules) - [Memory catalog](#memory-catalog) - [Attributes](#attributes-1) @@ -203,6 +205,10 @@ If a component does not provide a `memory` catalog, all memory-managed variables `{string: string | [string] | {path, when}} (default: {})`. Named key sets: what a foreign key elsewhere can name with `key`, when the instance it references can be one of several component types, or a key spanning several lists. See [Key sets](#key-sets). +#### `params` + +`{string: {type, from, rule}} (default: {})`. Binding parameters: what the component needs from whatever binds its file, such as an exchange's two models. See [Parameters](#parameters). + ### Component types Three component types can be distinguished: simulation, model, and package. @@ -393,6 +399,8 @@ A field with a `when` renders in brackets, like an optional one, e.g. `[] A [key set](#key-sets) may have a `when` too, which switches the component's part in a role on or off in the same way. +A `when` may instead name one of the component's [parameters](#parameters). The field is then switched on when the parameter is set where the file is bound. `gwf-mvr`'s `MODELNAMES` keyword has `when = "model2"` and isn't optional: MODFLOW 6 rejects a mover whose `MODELNAMES` disagrees with its binding, either way (`gwf-mvr.f90:785-798`), so it's set exactly when an exchange binds the mover. A writer can set it, and a user never needs to. + On an [observation](#observations) field, `when` names an option of the component declaring the observation, which is the observation's target, not of `utl-obs`. An observation can name a boundary only if its target sets `BOUNDNAMES`, so a `boundname` form has `when = "boundnames"`, and a component with no such option, like `gwf-chdg`, doesn't offer one. This is about whether a field is present at all, not how many values it holds. A column sized by another field, like `aux` (one value per auxiliary variable), uses `shape` instead, and a size of 0 means it's absent. @@ -525,7 +533,7 @@ A cellid is only valid as a column in a list item, where each row names its cell ###### `model` -`"1" | "2" | null (default: null)`. Which model's grid the cellid refers to: `null` for the grid of the component's own model; `"1"` or `"2"` for the grid of the first or second model of the enclosing context, i.e. `EXGMNAMEA` or `EXGMNAMEB` under an exchange. Under a single model, both are that model, so a component read in either context (e.g. `gwf-gnc`) gives the value unconditionally. A cellid in an exchange ([`subtype`](#subtype) `"exchange"`) must give `"1"` or `"2"`, since an exchange has no grid of its own, and a list item giving `"1"` or `"2"` on one cellid column must give one on every cellid column. For example, in an exchange `cellidm1` is `"1"` and `cellidm2` is `"2"`; in `gwf-gnc`, `cellidn` and `cellidsj` are `"1"` and `cellidm` is `"2"`. The position of a column says nothing about its model: GNC's columns, in order, refer to models 1, 2 and 1. +`string | null (default: null)`. Which model's grid the cellid refers to: `null` for the grid of the component's own model, or the name of one of the component's [parameters](#parameters) whose type is a model, for that model's grid. A parameter left unset where the file is bound means the component's own model too, so a component read in either context (e.g. `gwf-gnc`, under an exchange or a model) gives the value unconditionally. A cellid in an exchange ([`subtype`](#subtype) `"exchange"`) must name one, since an exchange has no grid of its own, and a list item naming a model on one cellid column must name one on every cellid column. For example, in an exchange `cellidm1` is `"model1"` and `cellidm2` is `"model2"`, set from `EXGMNAMEA` and `EXGMNAMEB`; in `gwf-gnc`, `cellidn` and `cellidsj` are `"model1"` and `cellidm` is `"model2"`. The position of a column says nothing about its model: GNC's columns, in order, refer to models 1, 2 and 1. **Note:** The values are strings, not integers, as `true == 1` in many languages (e.g. Python). A reader rejects an integer or boolean `model`. @@ -960,7 +968,7 @@ bound = "<=maxbound" In relational terms, a role is a supertype table: each declaring component adds its keys to it, keyed by (instance, key), and an `fk` with `key` is an ordinary foreign key to it. -An `fk` names a role with `key`, and picks the instance with a scope, as for a scoped key across components (or with a [rule](#rules)): +An `fk` names a role with `key`, and picks the instance with a scope, as for a scoped key across components, or with a [parameter](#parameters) (or a [rule](#rules)): ```toml # gwf-mvr period id1 / id2 @@ -987,7 +995,7 @@ A key set may be switched on by an option, written `{ path = "", when = "< A component that doesn't declare a role can't be named by an `fk` with that `key`. A validator checks that: -- each `key` an `fk` uses is declared by at least one component its scope can name (with a rule, by any component); +- each `key` an `fk` uses is declared by at least one component its scope or parameter can name (with only a rule, by any component); - each declared `path` is valid in its component; - a `<=dim` path is referenced only by an integer column with `index: true`, and a `"block.field"` path by a column of the same type. @@ -1005,25 +1013,100 @@ These follow each package's `pakmvrobj%ar(nproviders, nreceivers)` call: `(maxbo ```toml # utl-spc period bndno: a bound of the flow package its SSM row names pk = true -fk = { key = "bound", rule = "ssm_package" } +fk = { key = "bound", param = "target" } ``` MODFLOW 6 checks `bndno` only against SPC's own `maxbound`, which must be at least the flow package's bound count (`TspSpc.f90:326-349`). An advanced package's bounds are its features or its connections. SFR and UZF have one per reach or UZF cell. LAK and MAW have one per connection, numbered through every feature's connections in feature order, NLAKECONN or `sum(ngwfnodes)` in all (`gwf-maw.f90:935`). SSM applies SPC to an advanced package only when no advanced transport package is paired with it (`tsp-ssm.f90:195`). Array-based packages (`gwf-rcha`, `gwf-evta`) declare no `bound`: MODFLOW 6 pairs them only with array-based SPC (`utl-spca`), which has no `bndno` (`TspSpc.f90:351-383`). -[Observations](#observations) use the same idea, keyed by observation type: each component says what an observation's ID names, on the target side. +[Observations](#observations) use the same idea, keyed by observation type: each component says what an observation's ID names, on the target side. The observation types themselves are a role too, under the reserved name `observations`: every component with [`observations`](#observations) declares it, and no component may declare a key set by that name. `utl-obs`'s `obstype` names one of its target's types: -#### Rules +```toml +# utl-obs continuous obstype +fk = { param = "target", key = "observations" } +``` -Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's mover may name only the exchange's two models; a transport model's SSM names packages of whichever flow model an exchange pairs it with. No path or sibling scope can say this. Instead, the `fk` names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. +#### Parameters -A rule names a subset of the `fk`'s target, and the value must be in it. In relational terms, the `fk` references a named view rather than the target table. A rule may stand in for a scope, or narrow a scoped target further: +Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's cellids are in its two models' grids, but the models are named on `sim-nam`'s `exchanges` row, not in the exchange's file. An exchange's mover may name only those two models. SPC applies to whichever flow package SSM's row names. So a component declares what it needs from whatever binds it, as **parameters**, in [`params`](#params): ```toml -# gwf-mvr packages.mname, period mname1/mname2: the exchange's two models -fk = { path = "sim-nam.models.mname", rule = "exchange_models" } +# exg-gwfgwf, and the other exchanges +[params.model1] +type = "model" +from = "row.exgmnamea" + +[params.model2] +type = "model" +from = "row.exgmnameb" + +# gwf-mvr, gwf-gnc +[params.model1] +type = "model" +from = { exg-gwfgwf = "parent.model1" } + +# utl-spc +[params.target] +type = "package" +from = "row.pname" +rule = "no_advanced_transport" + +# utl-obs +[params.target] +type = ["model", "package"] +from = "parent" +``` + +- `type` is what the parameter names, a selector as in [`parent`](#parent). +- `from` says where its value comes from: one source for every component that binds this one, or a table of sources keyed by binder. A binder with no entry leaves the parameter unset. A mover under a model has no exchange, so its `model1` and `model2` are unset, which means that model. +- `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). + +A source is one of these, and there's no lookup or search: + +| source | the value | used by | +|---|---|---| +| `parent` | the component that binds this file | `utl-obs` | +| `row.` | that column of the binding row, following its `fk` to an instance | `utl-spc`, `utl-spca`, the exchanges | +| `parent.` | a parameter of the binder, passed down one step | `gwf-mvr` and `gwf-gnc` under an exchange | + +The binding row is the list row whose [`file`](#file) field links this component, such as SSM's `fileinput` row or `sim-nam`'s `exchanges` row. + +Three things can name a parameter: + +- an `fk`'s `param`. Alone, the value names the instance a parameter is set to, one of several if it's a list: `gwf-mvr`'s `mname1` is `fk = { param = ["model1", "model2"] }`, one of the exchange's two models. With a `key`, the parameter picks the instance whose key set is meant: `utl-spc`'s `bndno` is `fk = { key = "bound", param = "target" }`. +- a cellid's [`model`](#model): `"model1"` or `"model2"`. +- a field's [`when`](#when): `gwf-mvr`'s `MODELNAMES` has `when = "model2"`. +A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC only to a list-based package, and array-based SPC (`utl-spca`) only to `gwf-rcha` and `gwf-evta` (`TspSpc.f90:353-375`). `utl-spca` says so with `type = ["gwf-rcha", "gwf-evta"]`, and `utl-spc` says so through `key = "bound"`, which RCHA and EVTA don't declare. + +In relational terms, a parameter is a column on the child's row with a foreign key, and naming one is an ordinary foreign key to it: + +| DFN | SQL | +|---|---| +| `[params.target]` on `utl-spc` | columns `flow_mname`, `flow_pname` on `spc` | +| `fk = { param = ["model1", "model2"] }` | a composite key to `exchange_models (exchange, mname)` | +| `fk = { param = "target", key = "bound" }` | a foreign key to the `bound` supertype table | +| `when = "model2"`, `optional = false` | `CHECK (modelnames = (model2 IS NOT NULL))` | + +A validator checks, for each component that binds one with parameters, that: + +- each parameter's `type` admits some component; +- each binder in a `from` table binds the component; +- a `parent` source's binder is of the parameter's type; +- a `row.` source's binding row has the column, with an `fk`, and the column can name something of the parameter's type; +- a `parent.` source's binder declares that parameter, and its type can name something of this one's; +- each `fk`, cellid `model` and `when` naming a parameter names one the component declares, and a cellid's is of a model. + +So an exchange's cellid always resolves, and so does a mover's model name. + +#### Rules + +Some constraints aren't about a binding, so no [parameter](#parameters) says them. A transport model's SSM names packages of whichever flow model an exchange pairs it with, which nothing that binds SSM supplies: it comes from an exchange naming the transport model, or from a budget file. A mover's packages must have `MOVER` set. Instead, the `fk` or parameter names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. + +A rule names a subset of the target, and the value must be in it. In relational terms, the `fk` references a named view rather than the target table. A rule may stand in for a scope, or narrow a scoped target further: + +```toml # gwt-ssm / gwe-ssm sources.pname and fileinput.pname pk = true fk = { path = "gwf-nam.packages.pname", rule = "flow_model" } @@ -1037,9 +1120,8 @@ The rest of the `fk` is checked as usual, by its scope or as if unscoped: its pa | rule | meaning | MODFLOW 6 | |---|---|---| -| `exchange_models` | one of the two models (`exgmnamea`, `exgmnameb`) of the exchange whose `MVR6` option binds this mover. In a model's own mover the model names are absent (`MODELNAMES` is rejected there). | `GwfExchangeMover.f90:87-93` | | `flow_model` | in the flow model that a GWF-GWT or GWF-GWE exchange pairs with this transport model (its `exgmnamea`, where the transport model is `exgmnameb`). With flows read from a budget file there is no such exchange, and the reference isn't checked. | `exg-gwfgwt.f90:124-129`, `exg-gwfgwe.f90:121-126`, `tsp-ssm.f90:800-811` | -| `ssm_package` | in the flow package named by the SSM `fileinput` row that binds this SPC file. If an advanced transport package is paired with that flow package, SSM doesn't apply the SPC file and the reference isn't checked. | `TspSpc.f90:326-349`, `tsp-ssm.f90:195` | +| `no_advanced_transport` | a flow package with no advanced transport package paired with it. SSM doesn't apply an SPC file bound to one that has, so the file isn't checked. On `utl-spc`'s and `utl-spca`'s `target` | `tsp-ssm.f90:195` | | `mover_enabled` | a package that plays `mover_provider` or `mover_receiver`: one whose mover key sets are switched on by `MOVER` (see [Key sets](#key-sets)) | `gwf-mvr.f90:942-971` | The names are fixed by the schema, so a misspelled rule is an error. A new rule is added to the schema and this table together. 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 602438d7..4b0d8a53 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -597,17 +597,19 @@ def _mark_lonely_pk(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: # only means "needs 1-based/0-based conversion"). In every entry here the list # field's own name matches its enclosing block's name. # -# Each column also refers to one of two models' grids (see `v2.CellModel`), which -# v1 only says in prose and MF6 hard-codes (IDM's `StructArray.f90` maps -# `CELLIDM1`/`CELLIDM2` to `EXGMNAMEA`/`EXGMNAMEB`; `GhostNode.f90` reads -# `cellidn` and `cellidsj` in model 1's grid, `cellidm` in model 2's). -_CELLID_FIELDS: dict[str, tuple[str, dict[str, Literal["1", "2"]]]] = { - "exg-chfgwf": ("exchangedata", {"cellidm1": "1", "cellidm2": "2"}), - "exg-gwegwe": ("exchangedata", {"cellidm1": "1", "cellidm2": "2"}), - "exg-gwfgwf": ("exchangedata", {"cellidm1": "1", "cellidm2": "2"}), - "exg-gwtgwt": ("exchangedata", {"cellidm1": "1", "cellidm2": "2"}), - "exg-olfgwf": ("exchangedata", {"cellidm1": "1", "cellidm2": "2"}), - "gwf-gnc": ("gncdata", {"cellidn": "1", "cellidm": "2", "cellidsj": "1"}), +# Each column also refers to one of two models' grids, named by a parameter +# (see `v2.CellModel`, `_PARAMS`), which v1 only says in prose and MF6 +# hard-codes (IDM's `StructArray.f90` maps `CELLIDM1`/`CELLIDM2` to +# `EXGMNAMEA`/`EXGMNAMEB`; `GhostNode.f90` reads `cellidn` and `cellidsj` in +# model 1's grid, `cellidm` in model 2's). +_EXG_CELLIDS = {"cellidm1": "model1", "cellidm2": "model2"} +_CELLID_FIELDS: dict[str, tuple[str, dict[str, str]]] = { + "exg-chfgwf": ("exchangedata", _EXG_CELLIDS), + "exg-gwegwe": ("exchangedata", _EXG_CELLIDS), + "exg-gwfgwf": ("exchangedata", _EXG_CELLIDS), + "exg-gwtgwt": ("exchangedata", _EXG_CELLIDS), + "exg-olfgwf": ("exchangedata", _EXG_CELLIDS), + "gwf-gnc": ("gncdata", {"cellidn": "model1", "cellidm": "model2", "cellidsj": "model1"}), } @@ -620,7 +622,7 @@ def _cellid_base(field: "v2.Integer | v2.Array") -> dict[str, Any]: return base -def _as_cellid(field: v2.InputField, model: Literal["1", "2"] | None = None) -> v2.InputField: +def _as_cellid(field: v2.InputField, model: str | None = None) -> v2.InputField: """Rewrite an integer column as a cellid in the given model's grid: a `CellID` if it's one cellid (a plain integer, or a v1 `shape (ncelldim)` array), else an array of them, shaped by what counts them (GNC's @@ -1699,10 +1701,14 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B "mname"}`, `fk = {path = "gwf-nam.packages.pname", scope = "mname", rule = "mover_enabled"}`. - `packages.mname`, `period.perioddata`'s `mname1`/`mname2`: - `fk = {path = "sim-nam.models.mname", rule = "exchange_models"}`: a - model, and one of the exchange's two, which MF6 requires - (`xmvr_check_packages`, `GwfExchangeMover.f90:87-93`). In a model's - mover they're absent, since MF6 rejects `MODELNAMES` there. + `fk = {param = ["model1", "model2"]}`: one of the exchange's two + models (see `_PARAMS`), which MF6 requires (`xmvr_check_packages`, + `GwfExchangeMover.f90:87-93`). In a model's mover they're absent, + since MF6 rejects `MODELNAMES` there. + - `options.modelnames`: `when = "model2"`, required: MF6 rejects a + mover whose `MODELNAMES` disagrees with its binding, either way + (`gwf-mvr.f90:785-798`), so it's set exactly when an exchange binds + the mover, and a writer can set it. - `pname1`/`pname2`: `fk = {path = "packages.pname", scope = "mname1"}` (`"mname2"`), since `mvr_rp` looks each provider/receiver up among the `packages` rows, fatal if unlisted. @@ -1738,7 +1744,18 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B ): return blocks - model = v2.ForeignKey(path="sim-nam.models.mname", rule="exchange_models") + model = v2.ForeignKey(param=("model1", "model2")) + options = blocks.get("options") + if options is not None and isinstance(options.fields.get("modelnames"), v2.Keyword): + modelnames = options.fields["modelnames"].model_copy( + update={"when": "model2", "optional": False} + ) + blocks = { + **blocks, + "options": options.model_copy( + update={"fields": {**options.fields, "modelnames": modelnames}} + ), + } blocks = _set_list_item_attrs( blocks, "packages", @@ -1804,6 +1821,48 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B } +# Binding parameters (see `v2.Param`): what a component needs from whatever +# binds its file, which v1 says only in prose. +# +# - An exchange's two models are `sim-nam`'s `exchanges` row's `exgmnamea` +# and `exgmnameb`, not in its own file. Its cellids name them. +# - An exchange's mover and GNC get them from the exchange, one explicit +# step down. Under a model they're unset, meaning that model. +# - SPC and SPCA apply to the flow package SSM's `fileinput` row names +# (`TspSpc.f90:326-349`), skipped if an advanced transport package is +# paired with it (`tsp-ssm.f90:195`). MF6 rejects array SPC on any but +# RCHA and EVTA, and list SPC on those (`TspSpc.f90:353-375`): SPCA says +# so by its type, SPC by its `bound` key, which RCHA and EVTA don't +# declare. +# - OBS observes whatever binds it. +_EXG_PARAMS = { + "model1": v2.Param(type="model", from_="row.exgmnamea"), + "model2": v2.Param(type="model", from_="row.exgmnameb"), +} +_EXG_FORWARD = { + "model1": v2.Param(type="model", from_={"exg-gwfgwf": "parent.model1"}), + "model2": v2.Param(type="model", from_={"exg-gwfgwf": "parent.model2"}), +} +_PARAMS: dict[str, dict[str, v2.Param]] = { + **dict.fromkeys( + ("exg-chfgwf", "exg-gwegwe", "exg-gwfgwf", "exg-gwtgwt", "exg-olfgwf"), _EXG_PARAMS + ), + "gwf-mvr": _EXG_FORWARD, + "gwf-gnc": _EXG_FORWARD, + "utl-spc": { + "target": v2.Param(type="package", rule="no_advanced_transport", **{"from": "row.pname"}) + }, + "utl-spca": { + "target": v2.Param( + type=["gwf-rcha", "gwf-evta"], + rule="no_advanced_transport", + **{"from": "row.pname"}, + ) + }, + "utl-obs": {"target": v2.Param(type=["model", "package"], from_="parent")}, +} + + def _component_keys( name: str, blocks: dict[str, v2.Block], dims: Mapping[str, Any] ) -> dict[str, v2.KeySet] | None: @@ -1856,10 +1915,11 @@ def _ssm_pname_keyed(blocks: dict[str, v2.Block], block_name: str) -> bool: def _fix_spc_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: """ SPC's `bndno` names a bound of the flow package its SSM `fileinput` row - (its binding row) names, so `fk = {key = "bound", rule = - "ssm_package"}`, keeping its `pk` (one row per bound per period). MF6 reads `bndno` - against SPC's own `maxbound` and requires that to be at least the flow - package's bound count (`TspSpc.f90:326-349`). + (its binding row) names, its `target` parameter (see `_PARAMS`), so + `fk = {key = "bound", param = "target"}`, keeping its `pk` (one row per + bound per period). MF6 reads `bndno` against SPC's own `maxbound` and + requires that to be at least the flow package's bound count + (`TspSpc.f90:326-349`). Must run after `_mark_lonely_pk`, which marks only a column without an fk. """ @@ -1875,7 +1935,7 @@ def _fix_spc_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B return _set_list_item_attrs( blocks, "period", - {"bndno": {"fk": v2.ForeignKey(key=BOUND, rule="ssm_package")}}, + {"bndno": {"fk": v2.ForeignKey(key=BOUND, param="target")}}, list_name=period_list, ) @@ -2919,6 +2979,7 @@ def _lookup(rname: str) -> dict | None: "blocks": blocks or None, "dims": dims, "keys": _component_keys(name, blocks, dims or {}), + "params": _PARAMS.get(name), } if name == "sim-nam": return v2.Simulation(**d) diff --git a/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py b/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py index f2735424..6ccbe170 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py @@ -30,7 +30,11 @@ from boltons.dictutils import OMD from modflow_devtools.dfns import schema as v2 -from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import to_v2_0_0_dev2, try_parse_bool +from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import ( + _set_list_item_attrs, + to_v2_0_0_dev2, + try_parse_bool, +) from modflow_devtools.dfns.observations import OBSERVATIONS # --------------------------------------------------------------------------- @@ -1323,6 +1327,19 @@ def _numerical_model_memory( # --------------------------------------------------------------------------- +def _fix_obs_type(name: str, blocks: dict[str, v2.Block] | None) -> dict[str, v2.Block] | None: + """OBS's `obstype` names one of its target's observation types (the + `observations` tables this version adds), so `fk = {param = "target", + key = "observations"}`. MF6 rejects a type the target doesn't accept.""" + if name != "utl-obs" or not blocks: + return blocks + fk = v2.ForeignKey(param="target", key=v2.OBSERVATIONS_KEY) + for block_name in ("continuous", "single"): + if block_name in blocks: + blocks = _set_list_item_attrs(blocks, block_name, {"obstype": {"fk": fk}}) + return blocks + + def to_v2_0_0_dev3( name: str, fields: OMD, @@ -1478,7 +1495,7 @@ def to_v2_0_0_dev3( else: existing_runtime_dims[dim_name] = v2.RuntimeDim(set_in="ar") - blocks = component.blocks + blocks = _fix_obs_type(name, component.blocks) return component.model_copy( update={ diff --git a/modflow_devtools/dfns/schema.json b/modflow_devtools/dfns/schema.json index 5fd26561..c23a60ae 100644 --- a/modflow_devtools/dfns/schema.json +++ b/modflow_devtools/dfns/schema.json @@ -165,10 +165,6 @@ "model": { "anyOf": [ { - "enum": [ - "1", - "2" - ], "type": "string" }, { @@ -461,10 +457,6 @@ "model": { "anyOf": [ { - "enum": [ - "1", - "2" - ], "type": "string" }, { @@ -755,7 +747,7 @@ }, "ForeignKey": { "additionalProperties": false, - "description": "A reference to a `pk` column (or a string array's entries), one of:\n\n- `path`, `[component.]block.field`. With a `scope`, the name of a\n sibling column, that column qualifies the reference: within a\n component, it matches the target pk's own scope; across components, it\n picks the target component's instance. Written `fk = \"\"` when\n unscoped.\n- `key`, with a `scope` or `rule`: a key set (see `KeySet`) that the\n component of the picked instance declares, by name, in its `keys`. For\n an instance whose type is known only at runtime (MVR's `id1`).\n\nA `rule` (see `RULES`) names a subset of the target the value must fall\nin. It may stand in for a `scope` (MVR's `mname1`: `exchange_models`) or\nrestrict a scoped target further (MVR's `packages.pname`:\n`mover_enabled`).", + "description": "A reference to a `pk` column (or a string array's entries), one of:\n\n- `path`, `[component.]block.field`. With a `scope`, the name of a\n sibling column, that column qualifies the reference: within a\n component, it matches the target pk's own scope; across components, it\n picks the target component's instance. Written `fk = \"\"` when\n unscoped.\n- `key`, with a `scope`, `rule` or `param`: a key set (see `KeySet`)\n that the component of the picked instance declares, by name, in its\n `keys`. For an instance whose type is known only at runtime (MVR's\n `id1`). The reserved key `observations` is the instance's observation\n types (see `ComponentBase.observations`), which every component with\n any declares.\n- `param` alone, one or more of this component's parameters (see\n `Param`): the value names the instance one of them is set to (MVR's\n `mname1`: `model1` or `model2`).\n\nWith a `key`, `param` names the parameter whose instance declares the\nkey set (SPC's `bndno`: `target`).\n\nA `rule` (see `RULES`) names a subset of the target the value must fall\nin. It may stand in for a `scope` (SSM's `pname`: `flow_model`) or\nrestrict a scoped target further (MVR's `packages.pname`:\n`mover_enabled`).", "properties": { "path": { "anyOf": [ @@ -804,6 +796,24 @@ ], "default": null, "title": "Rule" + }, + "param": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Param" } }, "title": "ForeignKey", @@ -1751,6 +1761,21 @@ "default": null, "title": "Keys" }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/Param" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Params" + }, "type": { "const": "model", "default": "model", @@ -1996,6 +2021,21 @@ "default": null, "title": "Keys" }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/Param" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Params" + }, "type": { "const": "package", "default": "package", @@ -2033,6 +2073,58 @@ "title": "Package", "type": "object" }, + "Param": { + "additionalProperties": false, + "description": "A binding parameter: something a component needs from whatever binds its\nfile (see `docs/md/dfnspec.md`, \"Parameters\"). Declared in the\ncomponent's `params`, and named by an fk's `param`, a cellid's `model`,\nor a `when`.\n\n- `type`: what the parameter names, a selector as in `parent`.\n- `from`: where its value comes from, one source for every binder, or a\n table of sources keyed by binder. A binder with no entry leaves it\n unset. A source is one of:\n - `parent`: the component that binds this file (OBS's `target`);\n - `row.`: that column of the binding row, following its fk to\n an instance (SPC's `target`, from SSM's `pname`);\n - `parent.`: a parameter of the binder, passed down (an\n exchange's mover's `model1`).\n- `rule`: as on an fk (see `RULES`), a named subset of the target.\n\nThere's no lookup or search: each binder's source is stated.\nRelationally, a parameter is a column on the child's row, with a\nforeign key.", + "properties": { + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "title": "Type" + }, + "from": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + ], + "title": "From" + }, + "rule": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rule" + } + }, + "required": [ + "type", + "from" + ], + "title": "Param", + "type": "object" + }, "PrimaryKey": { "additionalProperties": false, "description": "A list item column's key: rows are unique by it. With a `scope`, the\nname of a sibling column, rows are unique by (scope, key) instead, e.g.\nMVR's `pname` within `mname`. Written `pk = true` when unscoped.", @@ -2442,6 +2534,21 @@ "default": null, "title": "Keys" }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/Param" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Params" + }, "type": { "const": "simulation", "default": "simulation", diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index f00106cc..254e2233 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -8,6 +8,8 @@ from boltons.dictutils import OMD from pydantic import ( + AfterValidator, + AliasChoices, BaseModel, BeforeValidator, ConfigDict, @@ -144,26 +146,22 @@ def _derive_tagged(self) -> bool: _IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") -# Named rules an fk can carry (see `ForeignKey.rule`): constraints the spec -# states in prose (`docs/md/dfnspec.md`, "Rules") and consumers implement in -# code, like JSON Schema's `format`. Each names a subset of the fk's target -# that the value must fall in, which no path or scope can say: most depend -# on where the component's file is bound in a simulation. A consumer that -# doesn't implement a rule skips its check. +# Named rules an fk or a parameter can carry (see `ForeignKey.rule`, +# `Param.rule`): constraints the spec states in prose (`docs/md/dfnspec.md`, +# "Rules") and consumers implement in code, like JSON Schema's `format`. +# Each names a subset of the target that the value must fall in, which no +# path, scope or parameter can say. A consumer that doesn't implement a rule +# skips its check. RULES: dict[str, str] = { - "exchange_models": ( - "one of the two models (exgmnamea, exgmnameb) of the exchange whose " - "MVR6 option binds this mover (GwfExchangeMover.f90:87-93)" - ), "flow_model": ( "in the flow model a GWF-GWT or GWF-GWE exchange pairs with this " "transport model (exg-gwfgwt.f90:124-129); unchecked with flows from " "a budget file" ), - "ssm_package": ( - "in the flow package named by the SSM fileinput row that binds this " - "SPC file (TspSpc.f90:326-349); unchecked if an advanced transport " - "package is paired with it (tsp-ssm.f90:195)" + "no_advanced_transport": ( + "a flow package with no advanced transport package paired with it, " + "since SSM then doesn't apply the SPC file it binds (tsp-ssm.f90:195); " + "with one, the file is unchecked" ), "mover_enabled": ( "a package that plays mover_provider or mover_receiver, i.e. whose " @@ -191,12 +189,21 @@ class ForeignKey(BaseModel): component, it matches the target pk's own scope; across components, it picks the target component's instance. Written `fk = ""` when unscoped. - - `key`, with a `scope` or `rule`: a key set (see `KeySet`) that the - component of the picked instance declares, by name, in its `keys`. For - an instance whose type is known only at runtime (MVR's `id1`). + - `key`, with a `scope`, `rule` or `param`: a key set (see `KeySet`) + that the component of the picked instance declares, by name, in its + `keys`. For an instance whose type is known only at runtime (MVR's + `id1`). The reserved key `observations` is the instance's observation + types (see `ComponentBase.observations`), which every component with + any declares. + - `param` alone, one or more of this component's parameters (see + `Param`): the value names the instance one of them is set to (MVR's + `mname1`: `model1` or `model2`). + + With a `key`, `param` names the parameter whose instance declares the + key set (SPC's `bndno`: `target`). A `rule` (see `RULES`) names a subset of the target the value must fall - in. It may stand in for a `scope` (MVR's `mname1`: `exchange_models`) or + in. It may stand in for a `scope` (SSM's `pname`: `flow_model`) or restrict a scoped target further (MVR's `packages.pname`: `mover_enabled`). """ @@ -207,6 +214,30 @@ class ForeignKey(BaseModel): scope: str | None = None key: str | None = None rule: str | None = None + param: str | tuple[str, ...] | None = None + + @field_validator("param", mode="before") + @classmethod + def _check_param(cls, v: Any) -> Any: + if isinstance(v, list | tuple): + v = tuple(v) + if not v: + raise ValueError("an fk's param needs at least one parameter") + if len(v) == 1: + v = v[0] + elif len(set(v)) != len(v): + raise ValueError(f"fk params {list(v)!r} repeat a parameter") + for name in (v,) if isinstance(v, str) else v or (): + if not isinstance(name, str) or not _IDENT_RE.match(name): + raise ValueError(f"fk param {name!r} must be a parameter name") + return v + + @property + def params(self) -> tuple[str, ...]: + """The parameters `param` names, none or more.""" + if self.param is None: + return () + return (self.param,) if isinstance(self.param, str) else self.param @field_validator("path") @classmethod @@ -236,10 +267,25 @@ def _check_rule(cls, v: str | None) -> str | None: @model_validator(mode="after") def _check_form(self) -> "ForeignKey": - if (self.path is None) == (self.key is None): - raise ValueError("an fk needs exactly one of path and key") - if self.key is not None and self.scope is None and self.rule is None: - raise ValueError("an fk with a key needs a scope or rule, which picks the instance") + if self.path is not None: + if self.key is not None or self.param is not None: + raise ValueError("an fk with a path can't have a key or param") + return self + if self.key is not None: + pickers = [a for a in ("scope", "param") if getattr(self, a) is not None] + if len(pickers) > 1: + raise ValueError("an fk with a key needs one of scope and param, not both") + if not pickers and self.rule is None: + raise ValueError( + "an fk with a key needs a scope, param or rule, which picks the instance" + ) + if self.param is not None and not isinstance(self.param, str): + raise ValueError("an fk with a key picks its instance by one param") + return self + if self.param is None: + raise ValueError("an fk needs one of path, key and param") + if self.scope is not None or self.rule is not None: + raise ValueError("an fk with only a param can't have a scope or rule") return self @property @@ -283,7 +329,12 @@ def _dump_pk(key: PrimaryKey) -> Any: def _dump_fk(key: ForeignKey) -> Any: - return key.path if key.simple else key.model_dump(exclude_none=True) + if key.simple: + return key.path + data = key.model_dump(exclude_none=True) + if isinstance(key.param, tuple): + data["param"] = list(key.param) + return data PK = Annotated[ @@ -364,6 +415,79 @@ def bound(self) -> str | None: return None +# The reserved key set name: an instance's observation types (see +# `ForeignKey`), which no component declares in `keys`. +OBSERVATIONS_KEY = "observations" + +_PARAM_SOURCE_RE = re.compile(r"^(?:parent(?:\.[A-Za-z_]\w*)?|row\.[A-Za-z_]\w*)$") + + +class Param(BaseModel): + """ + A binding parameter: something a component needs from whatever binds its + file (see `docs/md/dfnspec.md`, "Parameters"). Declared in the + component's `params`, and named by an fk's `param`, a cellid's `model`, + or a `when`. + + - `type`: what the parameter names, a selector as in `parent`. + - `from`: where its value comes from, one source for every binder, or a + table of sources keyed by binder. A binder with no entry leaves it + unset. A source is one of: + - `parent`: the component that binds this file (OBS's `target`); + - `row.`: that column of the binding row, following its fk to + an instance (SPC's `target`, from SSM's `pname`); + - `parent.`: a parameter of the binder, passed down (an + exchange's mover's `model1`). + - `rule`: as on an fk (see `RULES`), a named subset of the target. + + There's no lookup or search: each binder's source is stated. + Relationally, a parameter is a column on the child's row, with a + foreign key. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + type: str | list[str] + # `from` in a file, which Python reserves. + from_: str | dict[str, str] = PydanticField(validation_alias=AliasChoices("from", "from_")) + rule: str | None = None + + @field_validator("from_") + @classmethod + def _check_from(cls, v: str | dict[str, str]) -> str | dict[str, str]: + if isinstance(v, dict) and not v: + raise ValueError("a parameter's from table needs at least one binder") + for source in [v] if isinstance(v, str) else v.values(): + if not _PARAM_SOURCE_RE.match(source): + raise ValueError( + f"parameter source {source!r} must be 'parent', 'parent.' " + "or 'row.'" + ) + return v + + @field_validator("rule") + @classmethod + def _check_rule(cls, v: str | None) -> str | None: + if v is not None and v not in RULES: + raise ValueError(f"parameter rule {v!r} is not one of {sorted(RULES)!r}") + return v + + def source_for(self, binder: str) -> str | None: + """The source under the given binding component, or None if it + leaves the parameter unset.""" + return self.from_ if isinstance(self.from_, str) else self.from_.get(binder) + + +def _dump_param(p: Param) -> dict[str, Any]: + data: dict[str, Any] = {"type": p.type, "from": p.from_} + if p.rule is not None: + data["rule"] = p.rule + return data + + +ParamT = Annotated[Param, PlainSerializer(_dump_param)] + + def _dump_keyset(k: KeySet) -> Any: path = k.path if isinstance(k.path, str) else list(k.path) return path if k.when is None else {"path": path, "when": k.when} @@ -416,11 +540,18 @@ def _check_dynamic(self) -> "String": # only the field's description defines (e.g. SFR's `ic`). Index = bool | Literal["signed"] -# Which model's grid a cellid refers to. `None`: the component's own model. -# `"1"` / `"2"`: the first / second model of the enclosing context, i.e. -# `EXGMNAMEA` / `EXGMNAMEB` under an exchange; under a single model both are -# that model. -CellModel = Literal["1", "2"] | None + +def _check_cell_model(v: Any) -> Any: + if v is not None and not (isinstance(v, str) and _IDENT_RE.match(v)): + raise ValueError(f"a cellid's model {v!r} must be a parameter name") + return v + + +# Which model's grid a cellid refers to: the name of one of the component's +# parameters of type `model` (see `Param`), such as an exchange's `model1`, +# set from `EXGMNAMEA`. `None`, or a parameter left unset where the file is +# bound (GNC's `model1` under a model): the component's own model. +CellModel = Annotated[str | None, AfterValidator(_check_cell_model)] class Integer(InputFieldBase): @@ -1513,6 +1644,9 @@ class ComponentBase(BaseModel): # Named key sets other components' fks can name by `key`, without knowing # this component's type (MVR's `id1`: `mover_provider`). See `KeySet`. keys: dict[str, KeySetT] | None = None + # What this component needs from whatever binds its file, by name (an + # exchange's `model1`, SPC's `target`). See `Param`. + params: dict[str, ParamT] | None = None @field_validator("keys") @classmethod @@ -1520,6 +1654,16 @@ def _check_key_names(cls, v: dict[str, KeySet] | None) -> dict[str, KeySet] | No for name in v or {}: if not _IDENT_RE.match(name): raise ValueError(f"key set name {name!r} must be an identifier") + if name == OBSERVATIONS_KEY: + raise ValueError(f"key set name {name!r} is reserved for observation types") + return v + + @field_validator("params") + @classmethod + def _check_param_names(cls, v: dict[str, Param] | None) -> dict[str, Param] | None: + for name in v or {}: + if not _IDENT_RE.match(name): + raise ValueError(f"parameter name {name!r} must be an identifier") return v @model_serializer(mode="wrap") @@ -1833,6 +1977,15 @@ def _check_record_keys(fields: dict, outer: "Mapping[str, InputField] | None" = chain.append(scope) +def _param_components(spec: "Dfns", owner: "ComponentBase", name: str) -> set[str]: + """The components a parameter of `owner` can name, by its `type`, or + nothing if `owner` declares no such parameter.""" + param = (owner.params or {}).get(name) + if param is None: + return set() + return {n for n, c in spec.components.items() if admits(param.type, c)} + + def _named_components( spec: "Dfns", owner: "ComponentBase", @@ -1862,6 +2015,8 @@ def _named_components( for n, c in spec.components.items() if any(admits(s, c) for s in selectors) and admits(c.parent, owner) } + if fk is not None and fk.param is not None and fk.key is None: + return {n for p in fk.params for n in _param_components(spec, owner, p)} if fk is None or fk.path is None: return set() target = spec.components.get(fk.component) if fk.component else owner @@ -1901,10 +2056,27 @@ def _validate_fk_fields(component: "ComponentBase", spec: "Dfns") -> None: Observation fields (see `ObservationField`) are checked like a list item's. """ + def _check_param(field: "InputField", fk: ForeignKey) -> None: + for name in fk.params: + if name not in (component.params or {}): + raise ValueError( + f"Field {field.name!r} fk param={name!r}: {component.name} declares " + f"no such parameter" + ) + def _check_fk(field: "InputField", fk: ForeignKey, fields: dict) -> None: + _check_param(field, fk) if fk.key is not None: _check_key(field, fk, fields) return + if fk.path is None: + # A param alone: the value names the instance a parameter is set to. + if not isinstance(field, String): + raise ValueError( + f"Field {field.name!r} fk param={fk.param!r}: only a string column " + f"may name a parameter's instance" + ) + return assert fk.path is not None and fk.block is not None and fk.field is not None where_fk = f"Field {field.name!r} fk={fk.path!r}" target = component @@ -1960,9 +2132,19 @@ def _check_key(field: "InputField", fk: ForeignKey, fields: dict) -> None: if fk.scope is not None: named = sorted(_named_components(spec, component, fields, fields[fk.scope])) where_named = f"the scope {fk.scope!r} can name ({named!r})" + elif fk.param is not None: + assert isinstance(fk.param, str) + named = sorted(_param_components(spec, component, fk.param)) + where_named = f"the parameter {fk.param!r} can name" else: named = sorted(spec.components) where_named = "in the spec" + if fk.key == OBSERVATIONS_KEY: + if not isinstance(field, String): + raise ValueError(f"{where_fk}: observation types are named by a string column") + if not any(spec.components[n].observations for n in named): + raise ValueError(f"{where_fk}: no component {where_named} has observations") + return declaring = [spec.components[n] for n in named if fk.key in (spec.components[n].keys or {})] if not declaring: raise ValueError(f"{where_fk}: no component {where_named} declares key {fk.key!r}") @@ -2119,13 +2301,19 @@ def _validate_cellid_fields(component: "ComponentBase") -> None: meaningful as a column in a list item, where each row names its cell(s), or in an observation field, which reads like one. - Which grid a cellid refers to (its `model`) must be unambiguous: an - exchange's cellids refer to one of its two models, never its own, so each - must say which (`"1"` / `"2"`); and a list item naming either model on one - cellid column must name one on every cellid column (GNC's `cellidn`, - `cellidm`, `cellidsj`). + Which grid a cellid refers to (its `model`) must be unambiguous: a + `model` names one of the component's parameters whose type admits a + model (see `Param`); an exchange's cellids refer to one of its two + models, never its own, so each must name one; and a list item naming a + model on one cellid column must name one on every cellid column (GNC's + `cellidn`, `cellidm`, `cellidsj`). """ exchange = getattr(component, "subtype", None) == "exchange" + model_params = { + n + for n, p in (component.params or {}).items() + if any(t in ("model", "*") or t.endswith("-nam") for t in _parents_as_set(p.type)) + } def _cellids(field: "InputField", in_item: bool) -> "list[list[CellID | Array]]": """The cellid columns of each way a row may read `field`: one @@ -2140,7 +2328,12 @@ def _cellids(field: "InputField", in_item: bool) -> "list[list[CellID | Array]]" if exchange and field.model is None: raise ValueError( f"{type(field).__name__} {field.name!r}: an exchange's cellid must say " - f"which model's grid it refers to (model='1' or '2')" + f"which model's grid it refers to, by a parameter" + ) + if field.model is not None and field.model not in model_params: + raise ValueError( + f"{type(field).__name__} {field.name!r}: model={field.model!r} names no " + f"parameter of {component.name} whose type is a model" ) return [[field]] if isinstance(field, List): @@ -2160,8 +2353,8 @@ def _check_item(item: "InputField") -> None: if len({c.model is None for c in cellids}) > 1: names = {c.name: c.model for c in cellids} raise ValueError( - f"Cellid columns {names!r} mix model=None with '1'/'2': if one names " - f"a model, every cellid column in the item must" + f"Cellid columns {names!r} mix model=None with a named model: if one " + f"names a model, every cellid column in the item must" ) for block in (component.blocks or {}).values(): @@ -2173,16 +2366,93 @@ def _check_item(item: "InputField") -> None: def _validate_when_fields(component: "ComponentBase") -> None: """Every field's `when` (at any depth) names a keyword in the - component's options block.""" + component's options block, or one of its parameters (see `Param`), which + switches the field on when it's set. A keyword can't name itself.""" options = (component.blocks or {}).get("options") keywords = {n for n, f in (options.fields if options else {}).items() if isinstance(f, Keyword)} + names = keywords | set(component.params or {}) fields = [f for block in (component.blocks or {}).values() for f in block.fields.values()] for field in [*fields, *(component.observations or {}).values()]: for f in _iter_subfields(field): - if f.when is not None and f.when not in keywords: + if f.when is None: + continue + if f.when not in names: raise ValueError( - f"{f.name!r}: when={f.when!r} names no keyword option of {component.name}" + f"{f.name!r}: when={f.when!r} names no keyword option or parameter of " + f"{component.name}" ) + if f.when == f.name: + raise ValueError(f"{f.name!r}: when={f.when!r} names the field itself") + + +def _binders( + component: "ComponentBase", spec: "Dfns" +) -> "list[tuple[ComponentBase, Mapping[str, InputField]]]": + """Each component that can bind this one's file, with the binding row: + the fields beside the File that links it. A component's `parent` must + admit the binder too.""" + out: list[tuple[ComponentBase, Mapping[str, InputField]]] = [] + for binder in spec.components.values(): + if component.parent is not None and not admits(component.parent, binder): + continue + for block in (binder.blocks or {}).values(): + for file, siblings in _iter_files(block.fields): + if file.direction == "in" and file.component and admits(file.component, component): + out.append((binder, siblings)) + return out + + +def _validate_params(component: "ComponentBase", spec: "Dfns") -> None: + """ + Each parameter (see `Param`) resolves under every component that binds + this one: + + - its `type` admits some component in the spec; + - each binder named in a `from` table binds this component; + - `parent`: the binder is of the parameter's type; + - `row.`: the binding row has the column, with an fk, and what + the column can name (see `_named_components`) includes something of + the parameter's type; + - `parent.`: the binder declares that parameter, and its type + can name something of this one's type. + + Fks, cellids and `when`s that name a parameter are checked with the + fields they're on. + """ + if not component.params: + return + binders = _binders(component, spec) + binder_names = {b.name for b, _ in binders} + for name, param in component.params.items(): + where = f"{component.name}: parameter {name!r}" + admitted = _param_components(spec, component, name) + if not admitted: + raise ValueError(f"{where}: type={param.type!r} admits no component in the spec") + if isinstance(param.from_, dict): + if unknown := sorted(set(param.from_) - binder_names): + raise ValueError(f"{where}: {unknown!r} don't bind {component.name}") + for binder, row in binders: + source = param.source_for(binder.name) + if source is None: + continue + under = f"{where} under {binder.name}" + kind, _, rest = source.partition(".") + if kind == "parent" and not rest: + if not admits(param.type, binder): + raise ValueError(f"{under}: the binder isn't of type {param.type!r}") + elif kind == "parent": + if rest not in (binder.params or {}): + raise ValueError(f"{under}: {binder.name} declares no parameter {rest!r}") + if not _param_components(spec, binder, rest) & admitted: + raise ValueError(f"{under}: {binder.name}'s {rest!r} names no {param.type!r}") + else: + column = row.get(rest) + if column is None: + raise ValueError(f"{under}: the binding row has no column {rest!r}") + if getattr(column, "fk", None) is None: + raise ValueError(f"{under}: the binding row's {rest!r} has no fk to follow") + if not _named_components(spec, binder, row, column) & admitted: + raise ValueError(f"{under}: {rest!r} names nothing of type {param.type!r}") def _obs_columns(field: "InputField") -> list[int]: @@ -2539,6 +2809,8 @@ def _validate_relations(self) -> "Dfns": _validate_fk_fields(component, self) for name, component in self.components.items(): _validate_file_links(component, self) + for name, component in self.components.items(): + _validate_params(component, self) for name, component in self.components.items(): _validate_cellid_fields(component) for name, component in self.components.items(): From 216c8d75d622a770e333894a135feb12c7131c2a Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 10:56:48 -0800 Subject: [PATCH 02/11] refactor(dfns): pass binding parameters at the call site A component's `params` is now a signature, `{type, rule}`, and the file that binds it passes them in `args`, each naming a column of the binding row or one of the binder's own parameters. Drops `from` and its three source forms. Every exchange declares `model1`/`model2`. OBS needs no parameter: its target is the file's owner, which cross-component fks already resolve against. Drop obstype's fk and the reserved `observations` key set with it, and the fk form that picked a key's instance by a rule alone, which nothing uses now. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/v2.0.0.dev2/exg-chfgwf.json | 6 +- .../__snapshots__/v2.0.0.dev2/exg-chfgwf.toml | 2 - .../__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml | 2 - .../__snapshots__/v2.0.0.dev2/exg-gwegwe.json | 6 +- .../__snapshots__/v2.0.0.dev2/exg-gwegwe.toml | 2 - .../__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml | 2 - .../__snapshots__/v2.0.0.dev2/exg-gwfgwe.json | 10 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwe.toml | 6 + .../__snapshots__/v2.0.0.dev2/exg-gwfgwe.yaml | 5 + .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.json | 18 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml | 10 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml | 8 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwt.json | 10 +- .../__snapshots__/v2.0.0.dev2/exg-gwfgwt.toml | 6 + .../__snapshots__/v2.0.0.dev2/exg-gwfgwt.yaml | 5 + .../__snapshots__/v2.0.0.dev2/exg-gwfprt.json | 10 +- .../__snapshots__/v2.0.0.dev2/exg-gwfprt.toml | 6 + .../__snapshots__/v2.0.0.dev2/exg-gwfprt.yaml | 5 + .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.json | 6 +- .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml | 2 - .../__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml | 2 - .../__snapshots__/v2.0.0.dev2/exg-olfgwf.json | 6 +- .../__snapshots__/v2.0.0.dev2/exg-olfgwf.toml | 2 - .../__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml | 2 - .../__snapshots__/v2.0.0.dev2/gwe-ssm.json | 5 +- .../__snapshots__/v2.0.0.dev2/gwe-ssm.toml | 3 + .../__snapshots__/v2.0.0.dev2/gwe-ssm.yaml | 2 + .../__snapshots__/v2.0.0.dev2/gwf-gnc.json | 10 +- .../__snapshots__/v2.0.0.dev2/gwf-gnc.toml | 6 - .../__snapshots__/v2.0.0.dev2/gwf-gnc.yaml | 4 - .../__snapshots__/v2.0.0.dev2/gwf-mvr.json | 10 +- .../__snapshots__/v2.0.0.dev2/gwf-mvr.toml | 6 - .../__snapshots__/v2.0.0.dev2/gwf-mvr.yaml | 4 - .../__snapshots__/v2.0.0.dev2/gwt-ssm.json | 5 +- .../__snapshots__/v2.0.0.dev2/gwt-ssm.toml | 3 + .../__snapshots__/v2.0.0.dev2/gwt-ssm.yaml | 2 + .../__snapshots__/v2.0.0.dev2/sim-nam.json | 6 +- .../__snapshots__/v2.0.0.dev2/sim-nam.toml | 4 + .../__snapshots__/v2.0.0.dev2/sim-nam.yaml | 3 + .../__snapshots__/v2.0.0.dev2/utl-obs.json | 9 - .../__snapshots__/v2.0.0.dev2/utl-obs.toml | 7 - .../__snapshots__/v2.0.0.dev2/utl-obs.yaml | 6 - .../__snapshots__/v2.0.0.dev2/utl-spc.json | 1 - .../__snapshots__/v2.0.0.dev2/utl-spc.toml | 1 - .../__snapshots__/v2.0.0.dev2/utl-spc.yaml | 1 - .../__snapshots__/v2.0.0.dev2/utl-spca.json | 1 - .../__snapshots__/v2.0.0.dev2/utl-spca.toml | 1 - .../__snapshots__/v2.0.0.dev2/utl-spca.yaml | 1 - .../__snapshots__/v2.0.0.dev3/exg-chfgwf.json | 6 +- .../__snapshots__/v2.0.0.dev3/exg-chfgwf.toml | 2 - .../__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml | 2 - .../__snapshots__/v2.0.0.dev3/exg-gwegwe.json | 6 +- .../__snapshots__/v2.0.0.dev3/exg-gwegwe.toml | 2 - .../__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml | 2 - .../__snapshots__/v2.0.0.dev3/exg-gwfgwe.json | 10 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwe.toml | 6 + .../__snapshots__/v2.0.0.dev3/exg-gwfgwe.yaml | 5 + .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.json | 18 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml | 10 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml | 8 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwt.json | 10 +- .../__snapshots__/v2.0.0.dev3/exg-gwfgwt.toml | 6 + .../__snapshots__/v2.0.0.dev3/exg-gwfgwt.yaml | 5 + .../__snapshots__/v2.0.0.dev3/exg-gwfprt.json | 10 +- .../__snapshots__/v2.0.0.dev3/exg-gwfprt.toml | 6 + .../__snapshots__/v2.0.0.dev3/exg-gwfprt.yaml | 5 + .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.json | 6 +- .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml | 2 - .../__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml | 2 - .../__snapshots__/v2.0.0.dev3/exg-olfgwf.json | 6 +- .../__snapshots__/v2.0.0.dev3/exg-olfgwf.toml | 2 - .../__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml | 2 - .../__snapshots__/v2.0.0.dev3/gwe-ssm.json | 5 +- .../__snapshots__/v2.0.0.dev3/gwe-ssm.toml | 3 + .../__snapshots__/v2.0.0.dev3/gwe-ssm.yaml | 2 + .../__snapshots__/v2.0.0.dev3/gwf-gnc.json | 10 +- .../__snapshots__/v2.0.0.dev3/gwf-gnc.toml | 6 - .../__snapshots__/v2.0.0.dev3/gwf-gnc.yaml | 4 - .../__snapshots__/v2.0.0.dev3/gwf-mvr.json | 10 +- .../__snapshots__/v2.0.0.dev3/gwf-mvr.toml | 6 - .../__snapshots__/v2.0.0.dev3/gwf-mvr.yaml | 4 - .../__snapshots__/v2.0.0.dev3/gwt-ssm.json | 5 +- .../__snapshots__/v2.0.0.dev3/gwt-ssm.toml | 3 + .../__snapshots__/v2.0.0.dev3/gwt-ssm.yaml | 2 + .../__snapshots__/v2.0.0.dev3/sim-nam.json | 6 +- .../__snapshots__/v2.0.0.dev3/sim-nam.toml | 4 + .../__snapshots__/v2.0.0.dev3/sim-nam.yaml | 3 + .../__snapshots__/v2.0.0.dev3/utl-obs.json | 15 +- .../__snapshots__/v2.0.0.dev3/utl-obs.toml | 11 - .../__snapshots__/v2.0.0.dev3/utl-obs.yaml | 9 - .../__snapshots__/v2.0.0.dev3/utl-spc.json | 1 - .../__snapshots__/v2.0.0.dev3/utl-spc.toml | 1 - .../__snapshots__/v2.0.0.dev3/utl-spc.yaml | 1 - .../__snapshots__/v2.0.0.dev3/utl-spca.json | 1 - .../__snapshots__/v2.0.0.dev3/utl-spca.toml | 1 - .../__snapshots__/v2.0.0.dev3/utl-spca.yaml | 1 - autotest/dfns/test_migrate.py | 9 +- autotest/dfns/test_schema_relations.py | 171 ++++++------- docs/md/dfnspec.md | 68 +++--- .../dfns/migrate_to_v2_0_0_dev2.py | 74 +++--- .../dfns/migrate_to_v2_0_0_dev3.py | 21 +- modflow_devtools/dfns/schema.json | 36 +-- modflow_devtools/dfns/schema.py | 229 +++++++----------- 103 files changed, 515 insertions(+), 586 deletions(-) diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json index 0e5864c1..a41333ea 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.json @@ -108,12 +108,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml index 37e817e1..a260236d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.toml @@ -88,8 +88,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml index 7c653faf..7eb9c004 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-chfgwf.yaml @@ -109,7 +109,5 @@ blocks: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json index c469b65f..e9257826 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.json @@ -213,12 +213,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml index 63c7c0a1..9c7259a4 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.toml @@ -191,8 +191,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml index 6191d249..cd8b9c22 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwegwe.yaml @@ -214,7 +214,5 @@ blocks: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.json index 4d4a3320..002e624a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.json @@ -4,5 +4,13 @@ "name": "exg-gwfgwe", "parent": "sim-nam", "ftype": "GWF6-GWE6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.toml index 1888ec52..c6153d40 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.toml @@ -4,3 +4,9 @@ name = "exg-gwfgwe" parent = "sim-nam" ftype = "GWF6-GWE6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.yaml index cf70ac4b..9ef5d9f7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwe.yaml @@ -4,3 +4,8 @@ name: exg-gwfgwe parent: sim-nam ftype: GWF6-GWE6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json index d00e4ae2..3816ef5e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.json @@ -104,7 +104,11 @@ "description": "is the file name for ghost node correction input file. Information for the ghost nodes are provided in the file provided with these keywords. The format for specifying the ghost nodes is the same as described for the GNC Package of the GWF Model. This includes specifying OPTIONS, DIMENSIONS, and GNCDATA blocks. The order of the ghost nodes must follow the same order as the order of the cells in the EXCHANGEDATA block. For the GNCDATA, noden and all of the nodej values are assumed to be located in model 1, and nodem is assumed to be in model 2.", "tagged": false, "direction": "in", - "component": "gwf-gnc" + "component": "gwf-gnc", + "args": { + "model1": "model1", + "model2": "model2" + } } } }, @@ -123,7 +127,11 @@ "description": "is the file name of the water mover input file to apply to this exchange. Information for the water mover are provided in the file provided with these keywords. The format for specifying the water mover information is the same as described for the Water Mover (MVR) Package of the GWF Model, with two exceptions. First, in the PACKAGES block, the model name must be included as a separate string before each package. Second, the appropriate model name must be included before package name 1 and package name 2 in the BEGIN PERIOD block. This allows providers and receivers to be located in both models listed as part of this exchange.", "tagged": false, "direction": "in", - "component": "gwf-mvr" + "component": "gwf-mvr", + "args": { + "model1": "model1", + "model2": "model2" + } } } }, @@ -241,12 +249,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml index cab0a39e..fdf6bf47 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.toml @@ -99,6 +99,10 @@ tagged = false direction = "in" component = "gwf-gnc" +[blocks.options.fields.gnc_filerecord.fields.gnc6_filename.args] +model1 = "model1" +model2 = "model2" + [blocks.options.fields.mvr_filerecord] type = "record" optional = true @@ -116,6 +120,10 @@ tagged = false direction = "in" component = "gwf-mvr" +[blocks.options.fields.mvr_filerecord.fields.mvr6_filename.args] +model1 = "model1" +model2 = "model2" + [blocks.options.fields.obs_filerecord] type = "record" optional = true @@ -215,8 +223,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml index 9ff4dafd..1968c079 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwf.yaml @@ -127,6 +127,9 @@ blocks: tagged: false direction: in component: gwf-gnc + args: + model1: model1 + model2: model2 mvr_filerecord: type: record optional: true @@ -149,6 +152,9 @@ blocks: tagged: false direction: in component: gwf-mvr + args: + model1: model1 + model2: model2 obs_filerecord: type: record optional: true @@ -260,7 +266,5 @@ blocks: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.json index fd252853..aabddead 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.json @@ -4,5 +4,13 @@ "name": "exg-gwfgwt", "parent": "sim-nam", "ftype": "GWF6-GWT6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.toml index b5e385ed..6581e3cc 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.toml @@ -4,3 +4,9 @@ name = "exg-gwfgwt" parent = "sim-nam" ftype = "GWF6-GWT6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.yaml index a90667d7..8e5fc8da 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfgwt.yaml @@ -4,3 +4,8 @@ name: exg-gwfgwt parent: sim-nam ftype: GWF6-GWT6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.json index 1d9da0b6..6c038299 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.json @@ -4,5 +4,13 @@ "name": "exg-gwfprt", "parent": "sim-nam", "ftype": "GWF6-PRT6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.toml index 505a1090..c54c6ce4 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.toml @@ -4,3 +4,9 @@ name = "exg-gwfprt" parent = "sim-nam" ftype = "GWF6-PRT6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.yaml index d02cf7c7..6ca624c3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwfprt.yaml @@ -4,3 +4,8 @@ name: exg-gwfprt parent: sim-nam ftype: GWF6-PRT6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json index dcd93d18..73797ab4 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.json @@ -213,12 +213,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml index 485a1a32..c90cdde7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.toml @@ -191,8 +191,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml index 2dfa503e..8a52df27 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-gwtgwt.yaml @@ -212,7 +212,5 @@ blocks: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json index 00f9fa80..e976d0cc 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.json @@ -108,12 +108,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml index 48e2cca1..14695f9c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.toml @@ -88,8 +88,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml index b10cec16..fab5e9c4 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/exg-olfgwf.yaml @@ -109,7 +109,5 @@ blocks: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.json index af22159a..4139f8ec 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.json @@ -93,7 +93,10 @@ "component": [ "utl-spc", "utl-spca" - ] + ], + "args": { + "target": "pname" + } }, "mixed": { "type": "keyword", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.toml index 97584f46..7bc1ef8b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.toml @@ -85,6 +85,9 @@ component = [ "utl-spca", ] +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename.args] +target = "pname" + [blocks.fileinput.fields.fileinput.item.fields.mixed] type = "keyword" longname = "mixed keyword" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.yaml index 3e551e69..e7ebd4de 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwe-ssm.yaml @@ -106,6 +106,8 @@ blocks: component: - utl-spc - utl-spca + args: + target: pname mixed: type: keyword longname: mixed keyword diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json index 12569cf8..82a22591 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.json @@ -107,16 +107,10 @@ }, "params": { "model1": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model1" - } + "type": "model" }, "model2": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model2" - } + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml index c24b2bb0..dac6a150 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.toml @@ -90,11 +90,5 @@ shape = [ [params.model1] type = "model" -[params.model1.from] -exg-gwfgwf = "parent.model1" - [params.model2] type = "model" - -[params.model2.from] -exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml index d74df6f8..45cd369d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-gnc.yaml @@ -112,9 +112,5 @@ blocks: params: model1: type: model - from: - exg-gwfgwf: parent.model1 model2: type: model - from: - exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json index 63903b78..bd68e062 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.json @@ -235,16 +235,10 @@ }, "params": { "model1": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model1" - } + "type": "model" }, "model2": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model2" - } + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml index b82e5273..1d2f44de 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.toml @@ -208,11 +208,5 @@ index = true [params.model1] type = "model" -[params.model1.from] -exg-gwfgwf = "parent.model1" - [params.model2] type = "model" - -[params.model2.from] -exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml index 1a4b057a..ee851d04 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-mvr.yaml @@ -217,9 +217,5 @@ blocks: params: model1: type: model - from: - exg-gwfgwf: parent.model1 model2: type: model - from: - exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.json index 40c27d04..06203744 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.json @@ -93,7 +93,10 @@ "component": [ "utl-spc", "utl-spca" - ] + ], + "args": { + "target": "pname" + } }, "mixed": { "type": "keyword", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.toml index a3de2451..4ddc3b15 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.toml @@ -85,6 +85,9 @@ component = [ "utl-spca", ] +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename.args] +target = "pname" + [blocks.fileinput.fields.fileinput.item.fields.mixed] type = "keyword" longname = "mixed keyword" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.yaml index 387c47ab..1363a057 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwt-ssm.yaml @@ -106,6 +106,8 @@ blocks: component: - utl-spc - utl-spca + args: + target: pname mixed: type: keyword longname: mixed keyword diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json index 57e4ccb1..3cbb890b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json @@ -141,7 +141,11 @@ "direction": "in", "mode_keyword": false, "component": "exchange", - "component_ftype": "exgtype" + "component_ftype": "exgtype", + "args": { + "model1": "exgmnamea", + "model2": "exgmnameb" + } }, "exgmnamea": { "type": "string", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml index 06a6ce00..c1f8d447 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml @@ -125,6 +125,10 @@ mode_keyword = false component = "exchange" component_ftype = "exgtype" +[blocks.exchanges.fields.exchanges.item.fields.exgfile.args] +model1 = "exgmnamea" +model2 = "exgmnameb" + [blocks.exchanges.fields.exchanges.item.fields.exgmnamea] type = "string" longname = "name of model A" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml index 1f877d06..f92a4536 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml @@ -131,6 +131,9 @@ blocks: mode_keyword: false component: exchange component_ftype: exgtype + args: + model1: exgmnamea + model2: exgmnameb exgmnamea: type: string longname: name of model A 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 98224f56..98328002 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.json @@ -109,14 +109,5 @@ } } } - }, - "params": { - "target": { - "from": "parent", - "type": [ - "model", - "package" - ] - } } } \ No newline at end of file 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 44362e17..1e68cb00 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.toml @@ -89,10 +89,3 @@ direction = "out" type = "keyword" description = "an optional keyword used to indicate that the output file should be written in binary (unformatted) form." optional = true - -[params.target] -from = "parent" -type = [ - "model", - "package", -] 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 50e9c9db..eabdc90c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-obs.yaml @@ -104,9 +104,3 @@ blocks: description: an optional keyword used to indicate that the output file should be written in binary (unformatted) form. optional: true -params: - target: - from: parent - type: - - model - - package diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json index c1dfdbda..bc6735eb 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.json @@ -119,7 +119,6 @@ "params": { "target": { "type": "package", - "from": "row.pname", "rule": "no_advanced_transport" } } diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml index 8174119f..9fd4cc30 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.toml @@ -96,5 +96,4 @@ index = true [params.target] type = "package" -from = "row.pname" rule = "no_advanced_transport" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml index b94b4b7b..84d1658d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spc.yaml @@ -107,5 +107,4 @@ blocks: params: target: type: package - from: row.pname rule: no_advanced_transport diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json index 914e8407..9374ad11 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.json @@ -90,7 +90,6 @@ }, "params": { "target": { - "from": "row.pname", "rule": "no_advanced_transport", "type": [ "gwf-rcha", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml index 829e5939..aa43cdd0 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.toml @@ -77,7 +77,6 @@ tagged = false index = true [params.target] -from = "row.pname" rule = "no_advanced_transport" type = [ "gwf-rcha", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml index 84e77713..a9a2c16f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-spca.yaml @@ -86,7 +86,6 @@ blocks: index: true params: target: - from: row.pname rule: no_advanced_transport type: - gwf-rcha diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json index f4989499..9b44b914 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.json @@ -170,12 +170,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml index 2691cd05..4cefb01b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.toml @@ -148,8 +148,6 @@ shape = [ [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml index c549fb17..0467815c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-chfgwf.yaml @@ -160,7 +160,5 @@ memory: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json index 582d68ea..d5989069 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.json @@ -347,12 +347,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml index 64b4ed9d..0f5d0308 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.toml @@ -319,8 +319,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml index fdc98b16..0688166e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwegwe.yaml @@ -321,7 +321,5 @@ observations: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.json index 84441358..533c1619 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.json @@ -4,5 +4,13 @@ "name": "exg-gwfgwe", "parent": "sim-nam", "ftype": "GWF6-GWE6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.toml index 937210bd..363556d6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.toml @@ -4,3 +4,9 @@ name = "exg-gwfgwe" parent = "sim-nam" ftype = "GWF6-GWE6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.yaml index febe5521..4696d54f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwe.yaml @@ -4,3 +4,8 @@ name: exg-gwfgwe parent: sim-nam ftype: GWF6-GWE6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json index c17b70b3..ffeff2fb 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.json @@ -107,7 +107,11 @@ "description": "is the file name for ghost node correction input file. Information for the ghost nodes are provided in the file provided with these keywords. The format for specifying the ghost nodes is the same as described for the GNC Package of the GWF Model. This includes specifying OPTIONS, DIMENSIONS, and GNCDATA blocks. The order of the ghost nodes must follow the same order as the order of the cells in the EXCHANGEDATA block. For the GNCDATA, noden and all of the nodej values are assumed to be located in model 1, and nodem is assumed to be in model 2.", "tagged": false, "direction": "in", - "component": "gwf-gnc" + "component": "gwf-gnc", + "args": { + "model1": "model1", + "model2": "model2" + } } } }, @@ -126,7 +130,11 @@ "description": "is the file name of the water mover input file to apply to this exchange. Information for the water mover are provided in the file provided with these keywords. The format for specifying the water mover information is the same as described for the Water Mover (MVR) Package of the GWF Model, with two exceptions. First, in the PACKAGES block, the model name must be included as a separate string before each package. Second, the appropriate model name must be included before package name 1 and package name 2 in the BEGIN PERIOD block. This allows providers and receivers to be located in both models listed as part of this exchange.", "tagged": false, "direction": "in", - "component": "gwf-mvr" + "component": "gwf-mvr", + "args": { + "model1": "model1", + "model2": "model2" + } } } }, @@ -375,12 +383,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml index 18d1ac65..732abd11 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.toml @@ -102,6 +102,10 @@ tagged = false direction = "in" component = "gwf-gnc" +[blocks.options.fields.gnc_filerecord.fields.gnc6_filename.args] +model1 = "model1" +model2 = "model2" + [blocks.options.fields.mvr_filerecord] type = "record" optional = true @@ -119,6 +123,10 @@ tagged = false direction = "in" component = "gwf-mvr" +[blocks.options.fields.mvr_filerecord.fields.mvr6_filename.args] +model1 = "model1" +model2 = "model2" + [blocks.options.fields.obs_filerecord] type = "record" optional = true @@ -343,8 +351,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml index 4daf29e6..d0defd04 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwf.yaml @@ -129,6 +129,9 @@ blocks: tagged: false direction: in component: gwf-gnc + args: + model1: model1 + model2: model2 mvr_filerecord: type: record optional: true @@ -151,6 +154,9 @@ blocks: tagged: false direction: in component: gwf-mvr + args: + model1: model1 + model2: model2 obs_filerecord: type: record optional: true @@ -367,7 +373,5 @@ observations: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.json index bc5a0a97..d71f67e1 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.json @@ -4,5 +4,13 @@ "name": "exg-gwfgwt", "parent": "sim-nam", "ftype": "GWF6-GWT6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.toml index 7edb54fa..268377fa 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.toml @@ -4,3 +4,9 @@ name = "exg-gwfgwt" parent = "sim-nam" ftype = "GWF6-GWT6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.yaml index 293aa02e..5f2b0020 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfgwt.yaml @@ -4,3 +4,8 @@ name: exg-gwfgwt parent: sim-nam ftype: GWF6-GWT6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.json index 4a6daa1e..2475d51e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.json @@ -4,5 +4,13 @@ "name": "exg-gwfprt", "parent": "sim-nam", "ftype": "GWF6-PRT6", - "subtype": "exchange" + "subtype": "exchange", + "params": { + "model1": { + "type": "model" + }, + "model2": { + "type": "model" + } + } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.toml index 02148582..a44d5711 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.toml @@ -4,3 +4,9 @@ name = "exg-gwfprt" parent = "sim-nam" ftype = "GWF6-PRT6" subtype = "exchange" + +[params.model1] +type = "model" + +[params.model2] +type = "model" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.yaml index 63e7eefb..ddf21fda 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwfprt.yaml @@ -4,3 +4,8 @@ name: exg-gwfprt parent: sim-nam ftype: GWF6-PRT6 subtype: exchange +params: + model1: + type: model + model2: + type: model diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json index 29dc60e6..628ac50d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.json @@ -347,12 +347,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml index f32e0976..ddf1209f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.toml @@ -319,8 +319,6 @@ tagged = false [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml index 023714af..bea2b9e6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-gwtgwt.yaml @@ -319,7 +319,5 @@ observations: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json index d5de9451..1625d7ac 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.json @@ -170,12 +170,10 @@ }, "params": { "model1": { - "type": "model", - "from": "row.exgmnamea" + "type": "model" }, "model2": { - "type": "model", - "from": "row.exgmnameb" + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml index b27e9e21..d5c66877 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.toml @@ -148,8 +148,6 @@ shape = [ [params.model1] type = "model" -from = "row.exgmnamea" [params.model2] type = "model" -from = "row.exgmnameb" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml index c9a3f349..1f120793 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/exg-olfgwf.yaml @@ -160,7 +160,5 @@ memory: params: model1: type: model - from: row.exgmnamea model2: type: model - from: row.exgmnameb diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.json index f4c36e06..e065c37d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.json @@ -93,7 +93,10 @@ "component": [ "utl-spc", "utl-spca" - ] + ], + "args": { + "target": "pname" + } }, "mixed": { "type": "keyword", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.toml index f64d0117..cb8fdb5e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.toml @@ -85,6 +85,9 @@ component = [ "utl-spca", ] +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename.args] +target = "pname" + [blocks.fileinput.fields.fileinput.item.fields.mixed] type = "keyword" longname = "mixed keyword" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.yaml index ee5dea5d..98a143b3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwe-ssm.yaml @@ -106,6 +106,8 @@ blocks: component: - utl-spc - utl-spca + args: + target: pname mixed: type: keyword longname: mixed keyword diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json index f0799018..950d48b0 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.json @@ -119,16 +119,10 @@ }, "params": { "model1": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model1" - } + "type": "model" }, "model2": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model2" - } + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml index 96cfaa0a..2b581204 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.toml @@ -100,11 +100,5 @@ type = "integer" [params.model1] type = "model" -[params.model1.from] -exg-gwfgwf = "parent.model1" - [params.model2] type = "model" - -[params.model2.from] -exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml index 9df3f280..053b3a5b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-gnc.yaml @@ -121,9 +121,5 @@ memory: params: model1: type: model - from: - exg-gwfgwf: parent.model1 model2: type: model - from: - exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json index 06770e7e..8d66c6df 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.json @@ -247,16 +247,10 @@ }, "params": { "model1": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model1" - } + "type": "model" }, "model2": { - "type": "model", - "from": { - "exg-gwfgwf": "parent.model2" - } + "type": "model" } } } \ No newline at end of file diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml index 007552f6..76c43904 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.toml @@ -218,11 +218,5 @@ type = "integer" [params.model1] type = "model" -[params.model1.from] -exg-gwfgwf = "parent.model1" - [params.model2] type = "model" - -[params.model2.from] -exg-gwfgwf = "parent.model2" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml index e06810b3..1a7ce446 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-mvr.yaml @@ -226,9 +226,5 @@ memory: params: model1: type: model - from: - exg-gwfgwf: parent.model1 model2: type: model - from: - exg-gwfgwf: parent.model2 diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.json index 443592e6..4097cfb3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.json @@ -93,7 +93,10 @@ "component": [ "utl-spc", "utl-spca" - ] + ], + "args": { + "target": "pname" + } }, "mixed": { "type": "keyword", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.toml index 4ceb2d61..2be9d1aa 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.toml @@ -85,6 +85,9 @@ component = [ "utl-spca", ] +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename.args] +target = "pname" + [blocks.fileinput.fields.fileinput.item.fields.mixed] type = "keyword" longname = "mixed keyword" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.yaml index a801d3ba..cd3fa99f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwt-ssm.yaml @@ -106,6 +106,8 @@ blocks: component: - utl-spc - utl-spca + args: + target: pname mixed: type: keyword longname: mixed keyword diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json index 71011824..c13b429c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json @@ -141,7 +141,11 @@ "direction": "in", "mode_keyword": false, "component": "exchange", - "component_ftype": "exgtype" + "component_ftype": "exgtype", + "args": { + "model1": "exgmnamea", + "model2": "exgmnameb" + } }, "exgmnamea": { "type": "string", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml index 80a52341..64108353 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml @@ -125,6 +125,10 @@ mode_keyword = false component = "exchange" component_ftype = "exgtype" +[blocks.exchanges.fields.exchanges.item.fields.exgfile.args] +model1 = "exgmnamea" +model2 = "exgmnameb" + [blocks.exchanges.fields.exchanges.item.fields.exgmnamea] type = "string" longname = "name of model A" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml index 2f3c04fb..7563bdaf 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml @@ -131,6 +131,9 @@ blocks: mode_keyword: false component: exchange component_ftype: exgtype + args: + model1: exgmnamea + model2: exgmnameb exgmnamea: type: string longname: name of model A 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 5f903e8c..dd61d70a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.json @@ -42,11 +42,7 @@ "type": "string", "longname": "observation type", "description": "a string of characters used to identify the observation type.", - "tagged": false, - "fk": { - "key": "observations", - "param": "target" - } + "tagged": false }, "id": { "type": "union", @@ -113,14 +109,5 @@ } } } - }, - "params": { - "target": { - "from": "parent", - "type": [ - "model", - "package" - ] - } } } \ No newline at end of file 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 717328ff..b4f307bd 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.toml @@ -38,10 +38,6 @@ longname = "observation type" description = "a string of characters used to identify the observation type." tagged = false -[blocks.continuous.fields.continuous.item.fields.obstype.fk] -key = "observations" -param = "target" - [blocks.continuous.fields.continuous.item.fields.id] type = "union" longname = "time" @@ -93,10 +89,3 @@ direction = "out" type = "keyword" description = "an optional keyword used to indicate that the output file should be written in binary (unformatted) form." optional = true - -[params.target] -from = "parent" -type = [ - "model", - "package", -] 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 0fe88998..051b1b8b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-obs.yaml @@ -46,9 +46,6 @@ blocks: longname: observation type description: a string of characters used to identify the observation type. tagged: false - fk: - key: observations - param: target id: type: union longname: time @@ -107,9 +104,3 @@ blocks: description: an optional keyword used to indicate that the output file should be written in binary (unformatted) form. optional: true -params: - target: - from: parent - type: - - model - - package diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json index 31cbc966..7ae01ce0 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.json @@ -126,7 +126,6 @@ "params": { "target": { "type": "package", - "from": "row.pname", "rule": "no_advanced_transport" } } diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml index 23fcf686..1dd9689b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.toml @@ -101,5 +101,4 @@ type = "integer" [params.target] type = "package" -from = "row.pname" rule = "no_advanced_transport" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml index abaade19..6112ce10 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spc.yaml @@ -112,5 +112,4 @@ memory: params: target: type: package - from: row.pname rule: no_advanced_transport diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json index 6c7ed342..4a62d54e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.json @@ -90,7 +90,6 @@ }, "params": { "target": { - "from": "row.pname", "rule": "no_advanced_transport", "type": [ "gwf-rcha", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml index 8191e608..aea16f5e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.toml @@ -77,7 +77,6 @@ tagged = false index = true [params.target] -from = "row.pname" rule = "no_advanced_transport" type = [ "gwf-rcha", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml index 52ed98c7..d860c0bb 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-spca.yaml @@ -86,7 +86,6 @@ blocks: index: true params: target: - from: row.pname rule: no_advanced_transport type: - gwf-rcha diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 1297b571..8a62aba1 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -385,10 +385,7 @@ def test_migrate_mvr_keys(dfn_dir): # down from the exchange; unset under a model model = v2.ForeignKey(param=("model1", "model2")) assert packages["mname"].fk == model - assert component.params == { - f"model{i}": v2.Param(type="model", from_={"exg-gwfgwf": f"parent.model{i}"}) - for i in (1, 2) - } + assert component.params == {f"model{i}": v2.Param(type="model") for i in (1, 2)} # MODELNAMES is set exactly when an exchange binds the mover modelnames = blocks["options"].fields["modelnames"] assert (modelnames.when, modelnames.optional) == ("model2", False) @@ -524,9 +521,7 @@ def test_migrate_spc_bndno(dfn_dir): bndno = blocks["period"].fields["stress_period_data"].item.fields["bndno"] assert bndno.pk == v2.PrimaryKey() assert bndno.fk == v2.ForeignKey(key="bound", param="target") - assert component.params == { - "target": v2.Param(type="package", from_="row.pname", rule="no_advanced_transport") - } + assert component.params == {"target": v2.Param(type="package", rule="no_advanced_transport")} assert _fix_spc_relations("utl-spc", blocks) == blocks diff --git a/autotest/dfns/test_schema_relations.py b/autotest/dfns/test_schema_relations.py index bdb60430..78634dd4 100644 --- a/autotest/dfns/test_schema_relations.py +++ b/autotest/dfns/test_schema_relations.py @@ -2,6 +2,7 @@ import pytest +from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import _replace_named from modflow_devtools.dfns.schema import ( Array, Block, @@ -444,10 +445,7 @@ def test_array_fk_requires_integer_or_string_dtype(dtype): # A GNC's or an exchange's two models, as parameters (see `Param`). -_MODELS = { - "model1": Param(type="model", from_="row.exgmnamea"), - "model2": Param(type="model", from_="row.exgmnameb"), -} +_MODELS = {"model1": Param(type="model"), "model2": Param(type="model")} def _cellid_ctx(field, name="gwf-gnc", parent="gwf-nam", dims=None): @@ -585,7 +583,7 @@ def test_cellid_rejects_non_model_values(value, shape): _cellid_col("cellid", value, shape=shape) -@pytest.mark.parametrize("params", [None, {"model1": Param(type="package", from_="parent")}]) +@pytest.mark.parametrize("params", [None, {"model1": Param(type="package")}]) def test_dfns_validate_cellid_model_names_a_model_param(params): # The name must be a parameter of the component, and of a model. item = Record(name="item", fields={"a": _cellid_col("a", "model1")}) @@ -905,10 +903,6 @@ def test_integer_allow_zero_round_trips(): ), ({"key": "bound", "param": "target"}, ForeignKey(key="bound", param="target")), ({"param": ["model1", "model2"]}, ForeignKey(param=("model1", "model2"))), - ( - {"key": "observations", "param": "target"}, - ForeignKey(key="observations", param="target"), - ), ( {"path": "gwf-nam.packages.pname", "rule": "flow_model"}, ForeignKey(path="gwf-nam.packages.pname", rule="flow_model"), @@ -936,8 +930,9 @@ def test_key_fk_parts(): ({}, "needs one of path, key and param"), ({"path": "a.b", "key": "k", "scope": "s"}, "with a path can't have a key or param"), ({"path": "a.b", "param": "target"}, "with a path can't have a key or param"), - ({"key": "mover_provider"}, "needs a scope, param or rule"), - ({"key": "bound", "scope": "s", "param": "p"}, "one of scope and param, not both"), + ({"key": "mover_provider"}, "needs one of scope and param"), + ({"key": "bound", "rule": "flow_model"}, "needs one of scope and param"), + ({"key": "bound", "scope": "s", "param": "p"}, "needs one of scope and param"), ({"key": "bound", "param": ["p", "q"]}, "picks its instance by one param"), ({"param": "p", "scope": "s"}, "only a param can't have a scope or rule"), ({"param": "p", "rule": "flow_model"}, "only a param can't have a scope or rule"), @@ -1059,6 +1054,7 @@ def _ctx_sim(): mode_keyword=False, component="exchange", component_ftype="exgtype", + args={"model1": "exgmnamea", "model2": "exgmnameb"}, ), String(name="exgmnamea", tagged=False, fk="models.mname"), String(name="exgmnameb", tagged=False, fk="models.mname"), @@ -1089,7 +1085,11 @@ def _nam(name, ftype): "mvr6": Keyword(name="mvr6"), "filein": Keyword(name="filein"), "mvr6_filename": File( - name="mvr6_filename", tagged=False, direction="in", component="gwf-mvr" + name="mvr6_filename", + tagged=False, + direction="in", + component="gwf-mvr", + args={"model1": "model1", "model2": "model2"}, ), }, ) @@ -1147,10 +1147,7 @@ def _mover_pkgs(lak_keys=None, wel_keys=None, wel_options=("mover",)): _MNAME_FK = {"param": ["model1", "model2"]} # A mover's models, passed down from the exchange that binds it. -_FORWARD = { - "model1": Param(type="model", from_={"exg-gwfgwf": "parent.model1"}), - "model2": Param(type="model", from_={"exg-gwfgwf": "parent.model2"}), -} +_FORWARD = _MODELS _PNAME_FK = {"path": "gwf-nam.packages.pname", "scope": "mname", "rule": "mover_enabled"} @@ -1318,7 +1315,13 @@ def _ctx_transport(spc_key="bound", ssm_keys=_SOURCE, srctype=None): "fileinput", String(name="pname", tagged=False, pk=True, fk=pname_fk), Keyword(name="spc6"), - File(name="spc6_filename", tagged=False, direction="in", component="utl-spc"), + File( + name="spc6_filename", + tagged=False, + direction="in", + component="utl-spc", + args={"target": "pname"}, + ), ), }, ) @@ -1327,7 +1330,7 @@ def _ctx_transport(spc_key="bound", ssm_keys=_SOURCE, srctype=None): parent="gwt-ssm", subtype="utility", ftype="SPC6", - params={"target": Param(type="package", from_="row.pname", rule="no_advanced_transport")}, + params={"target": Param(type="package", rule="no_advanced_transport")}, blocks={ "period": _list_block( "period", @@ -1370,100 +1373,91 @@ def _with_params(ctx, name, **params): return {**ctx, name: component.model_copy(update={"params": merged})} +def _with_args(ctx, name, block, field, args): + """`ctx` with the args of component `name`'s File `field` replaced.""" + blocks = _replace_named( + name, + ctx[name].blocks, + block, + lambda f, _: f.model_copy(update={"args": args}), + {field}, + ) + return {**ctx, name: ctx[name].model_copy(update={"blocks": blocks})} + + def test_param_loads_and_dumps(): - toml = {"type": ["gwf-rcha", "gwf-evta"], "from": "row.pname", "rule": "no_advanced_transport"} + toml = {"type": ["gwf-rcha", "gwf-evta"], "rule": "no_advanced_transport"} pkg = Package.model_validate({"name": "utl-spca", "params": {"target": toml}}) assert pkg.params["target"] == Param( - type=["gwf-rcha", "gwf-evta"], from_="row.pname", rule="no_advanced_transport" + type=["gwf-rcha", "gwf-evta"], rule="no_advanced_transport" ) assert pkg.model_dump(exclude_none=True)["params"] == {"target": toml} - forward = {"type": "model", "from": {"exg-gwfgwf": "parent.model1"}} - assert Param.model_validate(forward).source_for("exg-gwfgwf") == "parent.model1" - assert Param.model_validate(forward).source_for("gwf-nam") is None - - -@pytest.mark.parametrize( - "source, match", - [ - ("row", "must be 'parent', 'parent.' or 'row.'"), - ("row.a.b", "must be 'parent', 'parent.' or 'row.'"), - ("parent.", "must be 'parent', 'parent.' or 'row.'"), - ("sibling.x", "must be 'parent', 'parent.' or 'row.'"), - ({}, "needs at least one binder"), - ({"exg-gwfgwf": "nope"}, "must be 'parent'"), - ], -) -def test_param_source_rejected(source, match): - with pytest.raises(ValueError, match=match): - Param(type="model", from_=source) def test_param_rule_checked(): with pytest.raises(ValueError, match="parameter rule 'nope' is not one of"): - Param(type="package", from_="parent", rule="nope") + Param(type="package", rule="nope") def test_param_name_checked(): with pytest.raises(ValueError, match="parameter name 'a-b' must be an identifier"): - Package(name="x", params={"a-b": Param(type="model", from_="parent")}) + Package(name="x", params={"a-b": Param(type="model")}) -def test_dfns_validate_params(): - """An exchange's models come from its binding row, a mover's from the - exchange, and SPC's target from SSM's row.""" +@pytest.mark.parametrize("args", [{"a": "b.c"}, {"a.b": "c"}, {"a": ""}]) +def test_file_args_checked(args): + with pytest.raises(ValueError, match="must name a column or parameter"): + File(name="f", direction="in", component="x", args=args) + + +def test_dfns_validate_args(): + """An exchange's models are passed from its binding row, a mover's from + the exchange's own, and SPC's target from SSM's row. A mover under a + model gets none.""" spec = Dfns(components=_ctx_transport()) assert spec.components["exg-gwfgwf"].params == _MODELS - assert spec.components["gwf-mvr"].params["model1"].source_for("gwf-nam") is None + item = spec.components["gwt-ssm"].blocks["fileinput"].fields["fileinput"].item + assert item.fields["spc6_filename"].args == {"target": "pname"} + + +_EXG = ("sim-nam", "exchanges", "exgfile") +_MVR = ("exg-gwfgwf", "options", "mvr6_filename") @pytest.mark.parametrize( - "name, param, match", + "where, args, match", [ ( - "exg-gwfgwf", - Param(type="model", from_="row.nope"), - "under sim-nam: the binding row has no column 'nope'", + _EXG, + {"model1": "nope"}, + "arg model1='nope' must name exactly one of a binding row column and a parameter " + "of sim-nam", ), - ( - "exg-gwfgwf", - Param(type="model", from_="row.exgtype"), - "the binding row's 'exgtype' has no fk to follow", - ), - ( - "gwf-mvr", - Param(type="model", from_={"exg-gwfgwf": "parent.model3"}), - "exg-gwfgwf declares no parameter 'model3'", - ), - ( - "gwf-mvr", - Param(type="model", from_={"exg-gwtgwt": "parent.model1"}), - r"\['exg-gwtgwt'\] don't bind gwf-mvr", - ), - ( - "gwf-mvr", - Param(type="model", from_="parent"), - "under exg-gwfgwf: the binder isn't of type 'model'", - ), - ( - "gwf-mvr", - Param(type="gwf-wel", from_={"exg-gwfgwf": "parent.model1"}), - "exg-gwfgwf's 'model1' names no 'gwf-wel'", - ), - ( - "exg-gwfgwf", - Param(type="package", from_="row.exgmnamea"), - "'exgmnamea' names nothing of type 'package'", - ), - ("exg-gwfgwf", Param(type="nope-nam", from_="row.exgmnamea"), "admits no component"), + (_EXG, {"model1": "exgtype"}, "arg model1='exgtype': the column has no fk to follow"), + (_EXG, {"model3": "exgmnamea"}, "passes 'model3', which exg-gwfgwf doesn't declare"), + (_MVR, {"model1": "model3"}, "must name exactly one of a binding row column"), + (_MVR, {"model3": "model1"}, "passes 'model3', which gwf-mvr doesn't declare"), ], ) -def test_dfns_validate_params_bad(name, param, match): +def test_dfns_validate_args_bad(where, args, match): with pytest.raises(ValueError, match=match): - Dfns(components=_with_params(_ctx_transport(), name, model1=param)) + Dfns(components=_with_args(_ctx_transport(), *where, args)) + + +@pytest.mark.parametrize( + "param, match", + [ + (Param(type="package"), "names nothing of type 'package'"), + (Param(type="nope-nam"), "admits no component"), + ], +) +def test_dfns_validate_params_bad(param, match): + with pytest.raises(ValueError, match=match): + Dfns(components=_with_params(_ctx_transport(), "exg-gwfgwf", model1=param)) def test_dfns_validate_fk_param_undeclared(): - ctx = _ctx_transport() + ctx = _with_args(_ctx_transport(), *_MVR, None) ctx["gwf-mvr"] = ctx["gwf-mvr"].model_copy(update={"params": None}) with pytest.raises(ValueError, match="fk param='model1': gwf-mvr declares no such parameter"): Dfns(components=ctx) @@ -1474,17 +1468,6 @@ def test_dfns_validate_fk_param_only_names_from_a_string(): Dfns(components=_ctx_mvr(id1_fk={"param": "model1"})) -def test_dfns_validate_observations_key_needs_a_string(): - ctx = _ctx_transport(spc_key="observations") - with pytest.raises(ValueError, match="observation types are named by a string column"): - Dfns(components=ctx) - - -def test_keyset_observations_reserved(): - with pytest.raises(ValueError, match="'observations' is reserved"): - Package(name="x", keys={"observations": "packagedata.ifno"}) - - def _mvr_with_modelnames(**attrs): ctx = _ctx_mvr() mvr = ctx["gwf-mvr"] diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index c6aca01b..06f116ee 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -207,7 +207,7 @@ If a component does not provide a `memory` catalog, all memory-managed variables #### `params` -`{string: {type, from, rule}} (default: {})`. Binding parameters: what the component needs from whatever binds its file, such as an exchange's two models. See [Parameters](#parameters). +`{string: {type, rule}} (default: {})`. Binding parameters: what the component needs from whatever binds its file, beyond the binder itself, such as an exchange's two models. The binding file passes them in [`args`](#args). See [Parameters](#parameters). ### Component types @@ -567,6 +567,10 @@ If a file option is introduced by one or more leading keywords (e.g. `CROSS_SECT Each component declares its token in [`ftype`](#ftype). +###### `args` + +`{string: string} (default: {})`. The arguments this binding passes the target's [parameters](#parameters), by name. Each names a column of the binding row, i.e. a sibling field, or one of this component's own parameters, passed down. For example, `sim-nam`'s `exchanges` rows have `exgfile` with `args = { model1 = "exgmnamea", model2 = "exgmnameb" }`. Only valid with `component`. + ### Composites Three kinds of composite type are relevant to MF6: [product](https://en.wikipedia.org/wiki/Product_type) (record), [sum](https://en.wikipedia.org/wiki/Tagged_union) (union), and collection (array, list). @@ -968,7 +972,7 @@ bound = "<=maxbound" In relational terms, a role is a supertype table: each declaring component adds its keys to it, keyed by (instance, key), and an `fk` with `key` is an ordinary foreign key to it. -An `fk` names a role with `key`, and picks the instance with a scope, as for a scoped key across components, or with a [parameter](#parameters) (or a [rule](#rules)): +An `fk` names a role with `key`, and picks the instance with a scope, as for a scoped key across components, or with a [parameter](#parameters): ```toml # gwf-mvr period id1 / id2 @@ -995,7 +999,7 @@ A key set may be switched on by an option, written `{ path = "", when = "< A component that doesn't declare a role can't be named by an `fk` with that `key`. A validator checks that: -- each `key` an `fk` uses is declared by at least one component its scope or parameter can name (with only a rule, by any component); +- each `key` an `fk` uses is declared by at least one component its scope or parameter can name; - each declared `path` is valid in its component; - a `<=dim` path is referenced only by an integer column with `index: true`, and a `"block.field"` path by a column of the same type. @@ -1020,57 +1024,43 @@ MODFLOW 6 checks `bndno` only against SPC's own `maxbound`, which must be at lea An advanced package's bounds are its features or its connections. SFR and UZF have one per reach or UZF cell. LAK and MAW have one per connection, numbered through every feature's connections in feature order, NLAKECONN or `sum(ngwfnodes)` in all (`gwf-maw.f90:935`). SSM applies SPC to an advanced package only when no advanced transport package is paired with it (`tsp-ssm.f90:195`). Array-based packages (`gwf-rcha`, `gwf-evta`) declare no `bound`: MODFLOW 6 pairs them only with array-based SPC (`utl-spca`), which has no `bndno` (`TspSpc.f90:351-383`). -[Observations](#observations) use the same idea, keyed by observation type: each component says what an observation's ID names, on the target side. The observation types themselves are a role too, under the reserved name `observations`: every component with [`observations`](#observations) declares it, and no component may declare a key set by that name. `utl-obs`'s `obstype` names one of its target's types: - -```toml -# utl-obs continuous obstype -fk = { param = "target", key = "observations" } -``` +[Observations](#observations) use the same idea, keyed by observation type: each component says what an observation's ID names, on the target side. #### Parameters -Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's cellids are in its two models' grids, but the models are named on `sim-nam`'s `exchanges` row, not in the exchange's file. An exchange's mover may name only those two models. SPC applies to whichever flow package SSM's row names. So a component declares what it needs from whatever binds it, as **parameters**, in [`params`](#params): +Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's cellids are in its two models' grids, but the models are named on `sim-nam`'s `exchanges` row, not in the exchange's file. An exchange's mover may name only those two models. SPC applies to whichever flow package SSM's row names. + +So binding works like a function call. A component declares what it needs, its **parameters**, in [`params`](#params), as a signature. The file that binds it passes them, as arguments, in its `file` field's [`args`](#args): ```toml -# exg-gwfgwf, and the other exchanges +# every exchange [params.model1] type = "model" -from = "row.exgmnamea" -[params.model2] -type = "model" -from = "row.exgmnameb" +# sim-nam exchanges row: passes the exchange its models +[blocks.exchanges.fields.exchanges.item.fields.exgfile] +args = { model1 = "exgmnamea", model2 = "exgmnameb" } -# gwf-mvr, gwf-gnc -[params.model1] -type = "model" -from = { exg-gwfgwf = "parent.model1" } +# gwf-mvr, gwf-gnc: the same parameters, passed down by exg-gwfgwf +[blocks.options.fields.mvr_filerecord.fields.mvr6_filename] +args = { model1 = "model1", model2 = "model2" } # utl-spc [params.target] type = "package" -from = "row.pname" rule = "no_advanced_transport" -# utl-obs -[params.target] -type = ["model", "package"] -from = "parent" +# gwt-ssm, gwe-ssm fileinput row +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename] +args = { target = "pname" } ``` - `type` is what the parameter names, a selector as in [`parent`](#parent). -- `from` says where its value comes from: one source for every component that binds this one, or a table of sources keyed by binder. A binder with no entry leaves the parameter unset. A mover under a model has no exchange, so its `model1` and `model2` are unset, which means that model. - `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). -A source is one of these, and there's no lookup or search: - -| source | the value | used by | -|---|---|---| -| `parent` | the component that binds this file | `utl-obs` | -| `row.` | that column of the binding row, following its `fk` to an instance | `utl-spc`, `utl-spca`, the exchanges | -| `parent.` | a parameter of the binder, passed down one step | `gwf-mvr` and `gwf-gnc` under an exchange | +An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. A binder that passes no argument leaves the parameter unset: a mover under a model gets no models, which means that model. -The binding row is the list row whose [`file`](#file) field links this component, such as SSM's `fileinput` row or `sim-nam`'s `exchanges` row. +The binder itself isn't a parameter. A file's owner is already known: it's what cross-component `fk`s resolve against (see [Primary and foreign keys](#primary-and-foreign-keys)), as a method's `self` is. So `utl-obs`, whose target is whatever binds it, declares none. Three things can name a parameter: @@ -1080,22 +1070,20 @@ Three things can name a parameter: A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC only to a list-based package, and array-based SPC (`utl-spca`) only to `gwf-rcha` and `gwf-evta` (`TspSpc.f90:353-375`). `utl-spca` says so with `type = ["gwf-rcha", "gwf-evta"]`, and `utl-spc` says so through `key = "bound"`, which RCHA and EVTA don't declare. -In relational terms, a parameter is a column on the child's row with a foreign key, and naming one is an ordinary foreign key to it: +In relational terms, an argument is a foreign key on the binding row, and the parameter is that column carried onto the child's row: | DFN | SQL | |---|---| -| `[params.target]` on `utl-spc` | columns `flow_mname`, `flow_pname` on `spc` | +| `args = { target = "pname" }` on SSM's `fileinput`, `[params.target]` on `utl-spc` | columns `flow_mname`, `flow_pname` on `spc`, from the `fileinput` row | | `fk = { param = ["model1", "model2"] }` | a composite key to `exchange_models (exchange, mname)` | | `fk = { param = "target", key = "bound" }` | a foreign key to the `bound` supertype table | | `when = "model2"`, `optional = false` | `CHECK (modelnames = (model2 IS NOT NULL))` | -A validator checks, for each component that binds one with parameters, that: +A validator checks that: - each parameter's `type` admits some component; -- each binder in a `from` table binds the component; -- a `parent` source's binder is of the parameter's type; -- a `row.` source's binding row has the column, with an `fk`, and the column can name something of the parameter's type; -- a `parent.` source's binder declares that parameter, and its type can name something of this one's; +- each argument is a parameter of every component its `file` can bind; +- each argument names exactly one of a binding row column, with an `fk`, and a binder's parameter, and can name something of the parameter's type; - each `fk`, cellid `model` and `when` naming a parameter names one the component declares, and a cellid's is of a model. So an exchange's cellid always resolves, and so does a mover's model name. 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 4b0d8a53..44d8f4e6 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1822,46 +1822,57 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B # Binding parameters (see `v2.Param`): what a component needs from whatever -# binds its file, which v1 says only in prose. +# binds its file, beyond the binder itself, which v1 says only in prose. The +# component declares them, like a function's signature; the binding file +# passes them (see `_ARGS`). # -# - An exchange's two models are `sim-nam`'s `exchanges` row's `exgmnamea` -# and `exgmnameb`, not in its own file. Its cellids name them. -# - An exchange's mover and GNC get them from the exchange, one explicit -# step down. Under a model they're unset, meaning that model. +# - Every exchange (`exg-*`) has two models, named on `sim-nam`'s +# `exchanges` row, not in its own file. Its cellids name them. +# - An exchange's mover and GNC get them from the exchange. Under a model +# they're unset, meaning that model. # - SPC and SPCA apply to the flow package SSM's `fileinput` row names # (`TspSpc.f90:326-349`), skipped if an advanced transport package is # paired with it (`tsp-ssm.f90:195`). MF6 rejects array SPC on any but # RCHA and EVTA, and list SPC on those (`TspSpc.f90:353-375`): SPCA says # so by its type, SPC by its `bound` key, which RCHA and EVTA don't # declare. -# - OBS observes whatever binds it. -_EXG_PARAMS = { - "model1": v2.Param(type="model", from_="row.exgmnamea"), - "model2": v2.Param(type="model", from_="row.exgmnameb"), -} -_EXG_FORWARD = { - "model1": v2.Param(type="model", from_={"exg-gwfgwf": "parent.model1"}), - "model2": v2.Param(type="model", from_={"exg-gwfgwf": "parent.model2"}), -} +_MODELS = {"model1": v2.Param(type="model"), "model2": v2.Param(type="model")} +_SPC_TARGET = "no_advanced_transport" _PARAMS: dict[str, dict[str, v2.Param]] = { - **dict.fromkeys( - ("exg-chfgwf", "exg-gwegwe", "exg-gwfgwf", "exg-gwtgwt", "exg-olfgwf"), _EXG_PARAMS - ), - "gwf-mvr": _EXG_FORWARD, - "gwf-gnc": _EXG_FORWARD, - "utl-spc": { - "target": v2.Param(type="package", rule="no_advanced_transport", **{"from": "row.pname"}) - }, - "utl-spca": { - "target": v2.Param( - type=["gwf-rcha", "gwf-evta"], - rule="no_advanced_transport", - **{"from": "row.pname"}, - ) - }, - "utl-obs": {"target": v2.Param(type=["model", "package"], from_="parent")}, + "gwf-mvr": _MODELS, + "gwf-gnc": _MODELS, + "utl-spc": {"target": v2.Param(type="package", rule=_SPC_TARGET)}, + "utl-spca": {"target": v2.Param(type=["gwf-rcha", "gwf-evta"], rule=_SPC_TARGET)}, } +# The arguments each binding file passes (see `v2.File.args`), as (block, +# file field, args): a column of the binding row, or the binder's own +# parameter, passed down. +_PASS_MODELS = {"model1": "model1", "model2": "model2"} +_ARGS: dict[str, list[tuple[str, str, dict[str, str]]]] = { + "sim-nam": [("exchanges", "exgfile", {"model1": "exgmnamea", "model2": "exgmnameb"})], + "exg-gwfgwf": [ + ("options", "gnc6_filename", _PASS_MODELS), + ("options", "mvr6_filename", _PASS_MODELS), + ], + "gwt-ssm": [("fileinput", "spc6_filename", {"target": "pname"})], + "gwe-ssm": [("fileinput", "spc6_filename", {"target": "pname"})], +} + + +def _fix_file_args(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + """Set the arguments this component's binding files pass. See `_ARGS`. + Skips an absent block, as a trimmed DFN may have.""" + for block_name, field_name, args in _ARGS.get(name, []): + if block_name not in blocks: + continue + + def _pass(field: Any, _in_union: bool, args: dict[str, str] = args) -> Any: + return field.model_copy(update={"args": args}) + + blocks = _replace_named(name, blocks, block_name, _pass, {field_name}) + return blocks + def _component_keys( name: str, blocks: dict[str, v2.Block], dims: Mapping[str, Any] @@ -2966,6 +2977,7 @@ def _lookup(rname: str) -> dict | None: blocks = _link_namefile_fields(name, blocks) blocks = _fix_model_nam_package_keys(name, blocks) blocks = _fix_spc_relations(name, blocks) + blocks = _fix_file_args(name, blocks) blocks = _fix_scoped_index_keys(name, blocks) blocks = _name_period_lists(blocks) blocks = _set_header_update(blocks) @@ -2979,7 +2991,7 @@ def _lookup(rname: str) -> dict | None: "blocks": blocks or None, "dims": dims, "keys": _component_keys(name, blocks, dims or {}), - "params": _PARAMS.get(name), + "params": _MODELS if name.startswith("exg-") else _PARAMS.get(name), } if name == "sim-nam": return v2.Simulation(**d) diff --git a/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py b/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py index 6ccbe170..f2735424 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev3.py @@ -30,11 +30,7 @@ from boltons.dictutils import OMD from modflow_devtools.dfns import schema as v2 -from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import ( - _set_list_item_attrs, - to_v2_0_0_dev2, - try_parse_bool, -) +from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import to_v2_0_0_dev2, try_parse_bool from modflow_devtools.dfns.observations import OBSERVATIONS # --------------------------------------------------------------------------- @@ -1327,19 +1323,6 @@ def _numerical_model_memory( # --------------------------------------------------------------------------- -def _fix_obs_type(name: str, blocks: dict[str, v2.Block] | None) -> dict[str, v2.Block] | None: - """OBS's `obstype` names one of its target's observation types (the - `observations` tables this version adds), so `fk = {param = "target", - key = "observations"}`. MF6 rejects a type the target doesn't accept.""" - if name != "utl-obs" or not blocks: - return blocks - fk = v2.ForeignKey(param="target", key=v2.OBSERVATIONS_KEY) - for block_name in ("continuous", "single"): - if block_name in blocks: - blocks = _set_list_item_attrs(blocks, block_name, {"obstype": {"fk": fk}}) - return blocks - - def to_v2_0_0_dev3( name: str, fields: OMD, @@ -1495,7 +1478,7 @@ def to_v2_0_0_dev3( else: existing_runtime_dims[dim_name] = v2.RuntimeDim(set_in="ar") - blocks = _fix_obs_type(name, component.blocks) + blocks = component.blocks return component.model_copy( update={ diff --git a/modflow_devtools/dfns/schema.json b/modflow_devtools/dfns/schema.json index c23a60ae..c2511a24 100644 --- a/modflow_devtools/dfns/schema.json +++ b/modflow_devtools/dfns/schema.json @@ -736,6 +736,21 @@ ], "default": null, "title": "Component Ftype" + }, + "args": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Args" } }, "required": [ @@ -747,7 +762,7 @@ }, "ForeignKey": { "additionalProperties": false, - "description": "A reference to a `pk` column (or a string array's entries), one of:\n\n- `path`, `[component.]block.field`. With a `scope`, the name of a\n sibling column, that column qualifies the reference: within a\n component, it matches the target pk's own scope; across components, it\n picks the target component's instance. Written `fk = \"\"` when\n unscoped.\n- `key`, with a `scope`, `rule` or `param`: a key set (see `KeySet`)\n that the component of the picked instance declares, by name, in its\n `keys`. For an instance whose type is known only at runtime (MVR's\n `id1`). The reserved key `observations` is the instance's observation\n types (see `ComponentBase.observations`), which every component with\n any declares.\n- `param` alone, one or more of this component's parameters (see\n `Param`): the value names the instance one of them is set to (MVR's\n `mname1`: `model1` or `model2`).\n\nWith a `key`, `param` names the parameter whose instance declares the\nkey set (SPC's `bndno`: `target`).\n\nA `rule` (see `RULES`) names a subset of the target the value must fall\nin. It may stand in for a `scope` (SSM's `pname`: `flow_model`) or\nrestrict a scoped target further (MVR's `packages.pname`:\n`mover_enabled`).", + "description": "A reference to a `pk` column (or a string array's entries), one of:\n\n- `path`, `[component.]block.field`. With a `scope`, the name of a\n sibling column, that column qualifies the reference: within a\n component, it matches the target pk's own scope; across components, it\n picks the target component's instance. Written `fk = \"\"` when\n unscoped.\n- `key`, with a `scope` or `param` that picks the instance: a key set\n (see `KeySet`) that the instance's component declares, by name, in\n its `keys`. For an instance whose type is known only at runtime\n (MVR's `id1`).\n- `param` alone, one or more of this component's parameters (see\n `Param`): the value names the instance one of them is set to (MVR's\n `mname1`: `model1` or `model2`).\n\nWith a `key`, `param` names the parameter whose instance declares the\nkey set (SPC's `bndno`: `target`).\n\nA `rule` (see `RULES`) names a subset of the target the value must fall\nin. It may stand in for a `scope` (SSM's `pname`: `flow_model`) or\nrestrict a scoped target further (MVR's `packages.pname`:\n`mover_enabled`).", "properties": { "path": { "anyOf": [ @@ -2075,7 +2090,7 @@ }, "Param": { "additionalProperties": false, - "description": "A binding parameter: something a component needs from whatever binds its\nfile (see `docs/md/dfnspec.md`, \"Parameters\"). Declared in the\ncomponent's `params`, and named by an fk's `param`, a cellid's `model`,\nor a `when`.\n\n- `type`: what the parameter names, a selector as in `parent`.\n- `from`: where its value comes from, one source for every binder, or a\n table of sources keyed by binder. A binder with no entry leaves it\n unset. A source is one of:\n - `parent`: the component that binds this file (OBS's `target`);\n - `row.`: that column of the binding row, following its fk to\n an instance (SPC's `target`, from SSM's `pname`);\n - `parent.`: a parameter of the binder, passed down (an\n exchange's mover's `model1`).\n- `rule`: as on an fk (see `RULES`), a named subset of the target.\n\nThere's no lookup or search: each binder's source is stated.\nRelationally, a parameter is a column on the child's row, with a\nforeign key.", + "description": "A binding parameter: something a component needs from whatever binds its\nfile, beyond the binder itself (see `docs/md/dfnspec.md`, \"Parameters\").\nDeclared in the component's `params`, like a function's signature, and\npassed by the binding file's `args` (see `File`). Named by an fk's\n`param`, a cellid's `model`, or a `when`.\n\n- `type`: what the parameter names, a selector as in `parent`.\n- `rule`: as on an fk (see `RULES`), a named subset of the target.\n\nA binder that passes no argument leaves it unset. Relationally, a\nparameter is a column on the child's row, with a foreign key.", "properties": { "type": { "anyOf": [ @@ -2091,20 +2106,6 @@ ], "title": "Type" }, - "from": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - ], - "title": "From" - }, "rule": { "anyOf": [ { @@ -2119,8 +2120,7 @@ } }, "required": [ - "type", - "from" + "type" ], "title": "Param", "type": "object" diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 254e2233..caefe0b9 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -9,7 +9,6 @@ from boltons.dictutils import OMD from pydantic import ( AfterValidator, - AliasChoices, BaseModel, BeforeValidator, ConfigDict, @@ -189,12 +188,10 @@ class ForeignKey(BaseModel): component, it matches the target pk's own scope; across components, it picks the target component's instance. Written `fk = ""` when unscoped. - - `key`, with a `scope`, `rule` or `param`: a key set (see `KeySet`) - that the component of the picked instance declares, by name, in its - `keys`. For an instance whose type is known only at runtime (MVR's - `id1`). The reserved key `observations` is the instance's observation - types (see `ComponentBase.observations`), which every component with - any declares. + - `key`, with a `scope` or `param` that picks the instance: a key set + (see `KeySet`) that the instance's component declares, by name, in + its `keys`. For an instance whose type is known only at runtime + (MVR's `id1`). - `param` alone, one or more of this component's parameters (see `Param`): the value names the instance one of them is set to (MVR's `mname1`: `model1` or `model2`). @@ -272,12 +269,9 @@ def _check_form(self) -> "ForeignKey": raise ValueError("an fk with a path can't have a key or param") return self if self.key is not None: - pickers = [a for a in ("scope", "param") if getattr(self, a) is not None] - if len(pickers) > 1: - raise ValueError("an fk with a key needs one of scope and param, not both") - if not pickers and self.rule is None: + if (self.scope is None) == (self.param is None): raise ValueError( - "an fk with a key needs a scope, param or rule, which picks the instance" + "an fk with a key needs one of scope and param, which picks the instance" ) if self.param is not None and not isinstance(self.param, str): raise ValueError("an fk with a key picks its instance by one param") @@ -415,56 +409,26 @@ def bound(self) -> str | None: return None -# The reserved key set name: an instance's observation types (see -# `ForeignKey`), which no component declares in `keys`. -OBSERVATIONS_KEY = "observations" - -_PARAM_SOURCE_RE = re.compile(r"^(?:parent(?:\.[A-Za-z_]\w*)?|row\.[A-Za-z_]\w*)$") - - class Param(BaseModel): """ A binding parameter: something a component needs from whatever binds its - file (see `docs/md/dfnspec.md`, "Parameters"). Declared in the - component's `params`, and named by an fk's `param`, a cellid's `model`, - or a `when`. + file, beyond the binder itself (see `docs/md/dfnspec.md`, "Parameters"). + Declared in the component's `params`, like a function's signature, and + passed by the binding file's `args` (see `File`). Named by an fk's + `param`, a cellid's `model`, or a `when`. - `type`: what the parameter names, a selector as in `parent`. - - `from`: where its value comes from, one source for every binder, or a - table of sources keyed by binder. A binder with no entry leaves it - unset. A source is one of: - - `parent`: the component that binds this file (OBS's `target`); - - `row.`: that column of the binding row, following its fk to - an instance (SPC's `target`, from SSM's `pname`); - - `parent.`: a parameter of the binder, passed down (an - exchange's mover's `model1`). - `rule`: as on an fk (see `RULES`), a named subset of the target. - There's no lookup or search: each binder's source is stated. - Relationally, a parameter is a column on the child's row, with a - foreign key. + A binder that passes no argument leaves it unset. Relationally, a + parameter is a column on the child's row, with a foreign key. """ model_config = ConfigDict(frozen=True, extra="forbid") type: str | list[str] - # `from` in a file, which Python reserves. - from_: str | dict[str, str] = PydanticField(validation_alias=AliasChoices("from", "from_")) rule: str | None = None - @field_validator("from_") - @classmethod - def _check_from(cls, v: str | dict[str, str]) -> str | dict[str, str]: - if isinstance(v, dict) and not v: - raise ValueError("a parameter's from table needs at least one binder") - for source in [v] if isinstance(v, str) else v.values(): - if not _PARAM_SOURCE_RE.match(source): - raise ValueError( - f"parameter source {source!r} must be 'parent', 'parent.' " - "or 'row.'" - ) - return v - @field_validator("rule") @classmethod def _check_rule(cls, v: str | None) -> str | None: @@ -472,21 +436,6 @@ def _check_rule(cls, v: str | None) -> str | None: raise ValueError(f"parameter rule {v!r} is not one of {sorted(RULES)!r}") return v - def source_for(self, binder: str) -> str | None: - """The source under the given binding component, or None if it - leaves the parameter unset.""" - return self.from_ if isinstance(self.from_, str) else self.from_.get(binder) - - -def _dump_param(p: Param) -> dict[str, Any]: - data: dict[str, Any] = {"type": p.type, "from": p.from_} - if p.rule is not None: - data["rule"] = p.rule - return data - - -ParamT = Annotated[Param, PlainSerializer(_dump_param)] - def _dump_keyset(k: KeySet) -> Any: path = k.path if isinstance(k.path, str) else list(k.path) @@ -606,6 +555,19 @@ class File(InputFieldBase): # A sibling String whose value is the target's ftype token (e.g. `DIS6`), # picking one family among the selector's matches. See `Dfns.ftype_family`. component_ftype: str | None = None + # The arguments this binding passes the target's parameters (see `Param`), + # by name: each a column of the binding row (`exgmnamea`), or a parameter + # of this component, passed down (an exchange's `model1`). See + # `_validate_params`. + args: dict[str, str] | None = None + + @field_validator("args") + @classmethod + def _check_args(cls, v: dict[str, str] | None) -> dict[str, str] | None: + for name, value in (v or {}).items(): + if not _IDENT_RE.match(name) or not _IDENT_RE.match(value): + raise ValueError(f"file arg {name!r} = {value!r} must name a column or parameter") + return v Scalar = Annotated[ @@ -1646,7 +1608,7 @@ class ComponentBase(BaseModel): keys: dict[str, KeySetT] | None = None # What this component needs from whatever binds its file, by name (an # exchange's `model1`, SPC's `target`). See `Param`. - params: dict[str, ParamT] | None = None + params: dict[str, Param] | None = None @field_validator("keys") @classmethod @@ -1654,8 +1616,6 @@ def _check_key_names(cls, v: dict[str, KeySet] | None) -> dict[str, KeySet] | No for name in v or {}: if not _IDENT_RE.match(name): raise ValueError(f"key set name {name!r} must be an identifier") - if name == OBSERVATIONS_KEY: - raise ValueError(f"key set name {name!r} is reserved for observation types") return v @field_validator("params") @@ -2132,19 +2092,10 @@ def _check_key(field: "InputField", fk: ForeignKey, fields: dict) -> None: if fk.scope is not None: named = sorted(_named_components(spec, component, fields, fields[fk.scope])) where_named = f"the scope {fk.scope!r} can name ({named!r})" - elif fk.param is not None: + else: assert isinstance(fk.param, str) named = sorted(_param_components(spec, component, fk.param)) where_named = f"the parameter {fk.param!r} can name" - else: - named = sorted(spec.components) - where_named = "in the spec" - if fk.key == OBSERVATIONS_KEY: - if not isinstance(field, String): - raise ValueError(f"{where_fk}: observation types are named by a string column") - if not any(spec.components[n].observations for n in named): - raise ValueError(f"{where_fk}: no component {where_named} has observations") - return declaring = [spec.components[n] for n in named if fk.key in (spec.components[n].keys or {})] if not declaring: raise ValueError(f"{where_fk}: no component {where_named} declares key {fk.key!r}") @@ -2385,74 +2336,68 @@ def _validate_when_fields(component: "ComponentBase") -> None: raise ValueError(f"{f.name!r}: when={f.when!r} names the field itself") -def _binders( - component: "ComponentBase", spec: "Dfns" -) -> "list[tuple[ComponentBase, Mapping[str, InputField]]]": - """Each component that can bind this one's file, with the binding row: - the fields beside the File that links it. A component's `parent` must - admit the binder too.""" - out: list[tuple[ComponentBase, Mapping[str, InputField]]] = [] - for binder in spec.components.values(): - if component.parent is not None and not admits(component.parent, binder): - continue - for block in (binder.blocks or {}).values(): - for file, siblings in _iter_files(block.fields): - if file.direction == "in" and file.component and admits(file.component, component): - out.append((binder, siblings)) - return out - - -def _validate_params(component: "ComponentBase", spec: "Dfns") -> None: +def _validate_args(component: "ComponentBase", spec: "Dfns") -> None: """ - Each parameter (see `Param`) resolves under every component that binds - this one: - - - its `type` admits some component in the spec; - - each binder named in a `from` table binds this component; - - `parent`: the binder is of the parameter's type; - - `row.`: the binding row has the column, with an fk, and what - the column can name (see `_named_components`) includes something of - the parameter's type; - - `parent.`: the binder declares that parameter, and its type - can name something of this one's type. - - Fks, cellids and `when`s that name a parameter are checked with the - fields they're on. + Each File's `args` pass parameters (see `Param`) of every component it + can bind, as a call passes a function's arguments: + + - each component the File can bind (see `Dfns.children`) declares each + parameter passed; + - each argument names exactly one of a column of the binding row (the + File's siblings) and a parameter of this component, so nothing is + searched; + - a column has an fk, and can name something of the parameter's type + (see `_named_components`); a parameter's type can name something of + the parameter's type. + + A parameter a binder doesn't pass is unset there. Fks, cellids and + `when`s that name a parameter are checked with the fields they're on. """ - if not component.params: - return - binders = _binders(component, spec) - binder_names = {b.name for b, _ in binders} - for name, param in component.params.items(): - where = f"{component.name}: parameter {name!r}" - admitted = _param_components(spec, component, name) - if not admitted: - raise ValueError(f"{where}: type={param.type!r} admits no component in the spec") - if isinstance(param.from_, dict): - if unknown := sorted(set(param.from_) - binder_names): - raise ValueError(f"{where}: {unknown!r} don't bind {component.name}") - for binder, row in binders: - source = param.source_for(binder.name) - if source is None: + for block in (component.blocks or {}).values(): + for file, row in _iter_files(block.fields): + if not file.args: continue - under = f"{where} under {binder.name}" - kind, _, rest = source.partition(".") - if kind == "parent" and not rest: - if not admits(param.type, binder): - raise ValueError(f"{under}: the binder isn't of type {param.type!r}") - elif kind == "parent": - if rest not in (binder.params or {}): - raise ValueError(f"{under}: {binder.name} declares no parameter {rest!r}") - if not _param_components(spec, binder, rest) & admitted: - raise ValueError(f"{under}: {binder.name}'s {rest!r} names no {param.type!r}") - else: - column = row.get(rest) - if column is None: - raise ValueError(f"{under}: the binding row has no column {rest!r}") - if getattr(column, "fk", None) is None: - raise ValueError(f"{under}: the binding row's {rest!r} has no fk to follow") - if not _named_components(spec, binder, row, column) & admitted: - raise ValueError(f"{under}: {rest!r} names nothing of type {param.type!r}") + where = f"{component.name}: File {file.name!r}" + if file.component is None: + raise ValueError(f"{where}: args without a component") + targets = [ + c for c in spec.children(component.name).values() if admits(file.component, c) + ] + for target in targets: + for name in file.args: + if name not in (target.params or {}): + raise ValueError( + f"{where}: passes {name!r}, which {target.name} doesn't declare" + ) + for name, value in file.args.items(): + at = f"{where}: arg {name}={value!r}" + column = row.get(value) + is_param = value in (component.params or {}) + if (column is None) == (not is_param): + raise ValueError( + f"{at} must name exactly one of a binding row column and a parameter " + f"of {component.name}" + ) + if is_param: + named = _param_components(spec, component, value) + else: + if getattr(column, "fk", None) is None: + raise ValueError(f"{at}: the column has no fk to follow") + named = _named_components(spec, component, row, column) + for target in targets: + param = (target.params or {})[name] + if not named & _param_components(spec, target, name): + raise ValueError(f"{at} names nothing of type {param.type!r}") + + +def _validate_params(component: "ComponentBase", spec: "Dfns") -> None: + """Each parameter's `type` admits some component in the spec.""" + for name, param in (component.params or {}).items(): + if not _param_components(spec, component, name): + raise ValueError( + f"{component.name}: parameter {name!r}: type={param.type!r} admits no " + f"component in the spec" + ) def _obs_columns(field: "InputField") -> list[int]: @@ -2811,6 +2756,8 @@ def _validate_relations(self) -> "Dfns": _validate_file_links(component, self) for name, component in self.components.items(): _validate_params(component, self) + for name, component in self.components.items(): + _validate_args(component, self) for name, component in self.components.items(): _validate_cellid_fields(component) for name, component in self.components.items(): From 896a2050f172bf1ac28444f5d0167a2c77022305 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 11:11:51 -0800 Subject: [PATCH 03/11] docs(dfns): unset parameters are null; an argument's fk rule applies Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 06f116ee..30fe7c05 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -533,7 +533,7 @@ A cellid is only valid as a column in a list item, where each row names its cell ###### `model` -`string | null (default: null)`. Which model's grid the cellid refers to: `null` for the grid of the component's own model, or the name of one of the component's [parameters](#parameters) whose type is a model, for that model's grid. A parameter left unset where the file is bound means the component's own model too, so a component read in either context (e.g. `gwf-gnc`, under an exchange or a model) gives the value unconditionally. A cellid in an exchange ([`subtype`](#subtype) `"exchange"`) must name one, since an exchange has no grid of its own, and a list item naming a model on one cellid column must name one on every cellid column. For example, in an exchange `cellidm1` is `"model1"` and `cellidm2` is `"model2"`, set from `EXGMNAMEA` and `EXGMNAMEB`; in `gwf-gnc`, `cellidn` and `cellidsj` are `"model1"` and `cellidm` is `"model2"`. The position of a column says nothing about its model: GNC's columns, in order, refer to models 1, 2 and 1. +`string | null (default: null)`. Which model's grid the cellid refers to: `null` for the grid of the component's own model, or the name of one of the component's [parameters](#parameters) whose type is a model, for that model's grid. A parameter that's unset where the file is bound is null, so it means the component's own model too, so a component read in either context (e.g. `gwf-gnc`, under an exchange or a model) gives the value unconditionally. A cellid in an exchange ([`subtype`](#subtype) `"exchange"`) must name one, since an exchange has no grid of its own, and a list item naming a model on one cellid column must name one on every cellid column. For example, in an exchange `cellidm1` is `"model1"` and `cellidm2` is `"model2"`, set from `EXGMNAMEA` and `EXGMNAMEB`; in `gwf-gnc`, `cellidn` and `cellidsj` are `"model1"` and `cellidm` is `"model2"`. The position of a column says nothing about its model: GNC's columns, in order, refer to models 1, 2 and 1. **Note:** The values are strings, not integers, as `true == 1` in many languages (e.g. Python). A reader rejects an integer or boolean `model`. @@ -1058,7 +1058,11 @@ args = { target = "pname" } - `type` is what the parameter names, a selector as in [`parent`](#parent). - `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). -An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. A binder that passes no argument leaves the parameter unset: a mover under a model gets no models, which means that model. +An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. + +Following an argument's `fk` applies all of it, [rule](#rules) included, and the parameter's own `rule` applies after that. SSM's `pname` has `rule = "flow_model"`, so the `target` it passes is a package of the paired flow model; `utl-spc`'s `rule = "no_advanced_transport"` then narrows it to one with no advanced transport package. + +A binder that passes no argument leaves the parameter unset, which is null, as for a field. Nothing fills it in from elsewhere. What null means is said by whatever names the parameter (below): a `when` naming it is off, and a cellid whose `model` names it is in the component's own model. So a mover under a model has no `MODELNAMES` or model names, and a GNC under a model reads all its cellids in that model. The binder itself isn't a parameter. A file's owner is already known: it's what cross-component `fk`s resolve against (see [Primary and foreign keys](#primary-and-foreign-keys)), as a method's `self` is. So `utl-obs`, whose target is whatever binds it, declares none. From 82a65e068ede2bce91a3a04960f7f2d2c3aece09 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 11:17:59 -0800 Subject: [PATCH 04/11] docs(dfns): parameters extend a component's scope Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 30fe7c05..8ae98b2f 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -1028,6 +1028,8 @@ An advanced package's bounds are its features or its connections. SFR and UZF ha #### Parameters +A reference resolves in a scope. Most resolve against the file's owner, the instance whose file names it (see [Primary and foreign keys](#primary-and-foreign-keys)): a package's references reach its own model, and an OBS file's IDs reach the package or model that names it. Parameters add to that scope what the owner can't know from its own file, because it's written on the row that names the file. They're the exception, and most components declare none. + Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's cellids are in its two models' grids, but the models are named on `sim-nam`'s `exchanges` row, not in the exchange's file. An exchange's mover may name only those two models. SPC applies to whichever flow package SSM's row names. So binding works like a function call. A component declares what it needs, its **parameters**, in [`params`](#params), as a signature. The file that binds it passes them, as arguments, in its `file` field's [`args`](#args): @@ -1058,7 +1060,7 @@ args = { target = "pname" } - `type` is what the parameter names, a selector as in [`parent`](#parent). - `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). -An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. +An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. The scoping is lexical: a binder can pass only what's in its own scope, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. Following an argument's `fk` applies all of it, [rule](#rules) included, and the parameter's own `rule` applies after that. SSM's `pname` has `rule = "flow_model"`, so the `target` it passes is a package of the paired flow model; `utl-spc`'s `rule = "no_advanced_transport"` then narrows it to one with no advanced transport package. From 02d7429be03fd731fbe82f5fe2b0147650d1b0fa Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 11:46:48 -0800 Subject: [PATCH 05/11] docs(dfns): explain bindings by case, before parameters Adds a Bindings section: terms (binding, parent, binding row, reference, context), every kind of binding in MODFLOW 6 with its input, and parameters as the cases that need them. Moves Parameters into it, and uses the same terms in the schema docstring and migration comments. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 244 ++++++++++++++---- .../dfns/migrate_to_v2_0_0_dev2.py | 16 +- modflow_devtools/dfns/schema.json | 2 +- modflow_devtools/dfns/schema.py | 8 +- 4 files changed, 212 insertions(+), 58 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 8ae98b2f..02a04c21 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -97,8 +97,11 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [Primary/foreign keys](#primary-and-foreign-keys) - [Scoped keys](#scoped-keys) - [Key sets](#key-sets) - - [Parameters](#parameters) - [Rules](#rules) + - [Bindings](#bindings) + - [Terms](#terms) + - [What a binding carries](#what-a-binding-carries) + - [Parameters](#parameters) - [Memory catalog](#memory-catalog) - [Attributes](#attributes-1) - [`type`](#type-2) @@ -173,7 +176,7 @@ A term **admits** a component if it equals the component's name, type, or subtyp **Note:** Terms may be mixed. A type or subtype subsumes any named component of the same type or subtype: e.g., `["gwf-sfr", "package"]` reduces to `["package"]` since `gwf-sfr` is a package. -A component that other components' input files link (see [File](#file)), such as a utility, has its `parent` derived from the components that link to it: the simplest selector that admits all of them. The links are the exact relation; such a `parent` covers them but may admit more (e.g. `utl-ts`'s `package` admits `gwf-npf`, which never links a time series file). +A component that other components' input files bind (see [Bindings](#bindings)), such as a utility, has its `parent` derived from the components that bind it: the simplest selector that admits all of them. The bindings are the exact relation; such a `parent` covers them but may admit more (e.g. `utl-ts`'s `package` admits `gwf-npf`, which never binds a time series file). #### `ftype` @@ -207,7 +210,7 @@ If a component does not provide a `memory` catalog, all memory-managed variables #### `params` -`{string: {type, rule}} (default: {})`. Binding parameters: what the component needs from whatever binds its file, beyond the binder itself, such as an exchange's two models. The binding file passes them in [`args`](#args). See [Parameters](#parameters). +`{string: {type, rule}} (default: {})`. What the component's references need from its binding, beyond its parent, such as an exchange's two models. The binding passes them in [`args`](#args). See [Parameters](#parameters). ### Component types @@ -569,7 +572,7 @@ Each component declares its token in [`ftype`](#ftype). ###### `args` -`{string: string} (default: {})`. The arguments this binding passes the target's [parameters](#parameters), by name. Each names a column of the binding row, i.e. a sibling field, or one of this component's own parameters, passed down. For example, `sim-nam`'s `exchanges` rows have `exgfile` with `args = { model1 = "exgmnamea", model2 = "exgmnameb" }`. Only valid with `component`. +`{string: string} (default: {})`. The arguments this [binding](#bindings) passes the target's [parameters](#parameters), by name. Each names a column of the binding row with an `fk`, i.e. a sibling field, or one of this component's own parameters, passed on. For example, `sim-nam`'s `exchanges` rows have `exgfile` with `args = { model1 = "exgmnamea", model2 = "exgmnameb" }`. Only valid with `component`. ### Composites @@ -900,7 +903,7 @@ An `fk` names an existing row: a file in which one names no row is invalid. An i An `fk` is a hierarchical path of the form `"[component.]block.field"`, naming the `pk` field it references. The `component` segment is necessary for cross-component references; it may be omitted for within-component references. On a `string` field, the path may instead name a string array (e.g. `"options.auxiliary"`), whose entries then act as the key set: the array is in effect a one-column list whose entries are its keys. A dynamic keyword (see [`dynamic`](#dynamic)) needs this form when its keywords are a string array's entries. -A cross-component `fk` resolves in the current context: against the singleton component (e.g. `sim-nam`), or the instance that owns this file (e.g. a reference from a model's package to that model's name file). +A cross-component `fk` resolves in the file's [context](#terms): against a singleton component (e.g. `sim-nam`), or an ancestor of the instance the file is input for (e.g. a reference from a model's package to that model's name file). #### Scoped keys @@ -1026,27 +1029,165 @@ An advanced package's bounds are its features or its connections. SFR and UZF ha [Observations](#observations) use the same idea, keyed by observation type: each component says what an observation's ID names, on the target side. -#### Parameters +#### Rules + +Some constraints aren't about a binding, so no [parameter](#parameters) says them. A transport model's SSM names packages of whichever flow model an exchange pairs it with, which nothing that binds SSM supplies: it comes from an exchange naming the transport model, or from a budget file. A mover's packages must have `MOVER` set. Instead, the `fk` or parameter names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. + +A rule names a subset of the target, and the value must be in it. In relational terms, the `fk` references a named view rather than the target table. A rule may stand in for a scope, or narrow a scoped target further: + +```toml +# gwt-ssm / gwe-ssm sources.pname and fileinput.pname +pk = true +fk = { path = "gwf-nam.packages.pname", rule = "flow_model" } + +# gwf-mvr packages.pname: the scope picks the model, the rule narrows its packages to movers +pk = { scope = "mname" } +fk = { path = "gwf-nam.packages.pname", scope = "mname", rule = "mover_enabled" } +``` + +The rest of the `fk` is checked as usual, by its scope or as if unscoped: its path must name a `pk` (or string array), and its `key` must be declared by some component. A consumer that doesn't implement a rule skips the check; the rest of the `fk` still says what kind of thing the value names. + +| rule | meaning | MODFLOW 6 | +|---|---|---| +| `flow_model` | in the flow model that a GWF-GWT or GWF-GWE exchange pairs with this transport model (its `exgmnamea`, where the transport model is `exgmnameb`). With flows read from a budget file there is no such exchange, and the reference isn't checked. | `exg-gwfgwt.f90:124-129`, `exg-gwfgwe.f90:121-126`, `tsp-ssm.f90:800-811` | +| `no_advanced_transport` | a flow package with no advanced transport package paired with it. SSM doesn't apply an SPC file bound to one that has, so the file isn't checked. On `utl-spc`'s and `utl-spca`'s `target` | `tsp-ssm.f90:195` | +| `mover_enabled` | a package that plays `mover_provider` or `mover_receiver`: one whose mover key sets are switched on by `MOVER` (see [Key sets](#key-sets)) | `gwf-mvr.f90:942-971` | + +The names are fixed by the schema, so a misspelled rule is an error. A new rule is added to the schema and this table together. + +### Bindings + +A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model name files, the exchanges and the solutions; a model name file names its packages; a package's options name its utility files, such as time series and observations. This section says how the DFNs describe that tree, and what a file can know about where it sits in it. + +#### Terms -A reference resolves in a scope. Most resolve against the file's owner, the instance whose file names it (see [Primary and foreign keys](#primary-and-foreign-keys)): a package's references reach its own model, and an OBS file's IDs reach the package or model that names it. Parameters add to that scope what the owner can't know from its own file, because it's written on the row that names the file. They're the exception, and most components declare none. +- A **binding** is a `file` field with a [`component`](#component), and each value of it: a file named as the input of an instance of the component. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. +- The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. +- The **binding row** is the record holding the binding field: here, the whole `GWF6 model.nam gwf_1` line. Its other columns may say something about the child, such as its name. +- A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. A binding names a file, and so builds the tree. A reference names something in the tree, and is resolved once the tree is built. +- A reference resolves in its component's **context**: the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. Context is what programming languages call scope. This spec says context because it already uses scope for [scoped keys](#scoped-keys) and [dimension scope](#dimension-scope). -Some references depend on where a component's file is bound in a simulation, not only on the data in it. An exchange's cellids are in its two models' grids, but the models are named on `sim-nam`'s `exchanges` row, not in the exchange's file. An exchange's mover may name only those two models. SPC applies to whichever flow package SSM's row names. +So a component's file says what it says, and its binding row may say more about it. The question in each case is whether the file's references need any of that. -So binding works like a function call. A component declares what it needs, its **parameters**, in [`params`](#params), as a signature. The file that binds it passes them, as arguments, in its `file` field's [`args`](#args): +#### What a binding carries + +Every binding in MODFLOW 6 is one of these cases. + +| | Case | Example | The child needs | Said with | +|---|---|---|---|---| +| 1 | A file, and nothing else | `OBS6 FILEIN chd.obs` | nothing | [`component`](#component) | +| 2 | The row names the child | `CHD6 model.chd CHD-1` | nothing | a `pk` on the row | +| 3 | The row says more, which the child doesn't use | `1 TAB6 FILEIN lake1.tab` | nothing | nothing | +| 4 | The row says what the child's references need | `GWF6-GWF6 model.gwfgwf gwf_1 gwf_2` | the exchange's models | `params`, `args` | +| 5 | The parent passes on what it was passed | `GNC6 FILEIN model.gnc` in an exchange | the exchange's models | `args` naming a parameter | +| 6 | One component, bound under different parents | `GNC6 model.gnc` in a model name file | nothing | no `args`: unset | +| 7 | The row names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | +| 8 | No binding says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | + +**1. A file, and nothing else.** Most bindings name a file and its component, and that's all the child needs. Its references resolve against its parent: + +``` +# a CHD package's options +TS6 FILEIN chd.ts +OBS6 FILEIN chd.obs + +# chd.obs +BEGIN continuous FILEOUT chd.obs.csv + q-west chd west +END continuous +``` + +`west` names a CHD boundary, as CHD's [`[observations]`](#observations) declares, and an observation by cellid is in the CHD's model's grid. `utl-obs` is bound by 52 components and needs nothing from any of them: whatever binds it is what it observes. References can also point down the tree: a CHD head given as a time series name names a series in the TS file it binds. + +**2. The row names the child.** A name file's row gives the child its name: + +``` +# a GWF model name file's packages +CHD6 model.chd CHD-1 +``` + +`pname` is a key of the row (`pk`), and other files refer to the package by it, within its model: a mover's `packages`, a transport model's SSM. The name is the parent's data, though, and the CHD file never refers to itself, so the child needs nothing. `mfsim.nam`'s `models` rows name models the same way, and its `solutiongroup` rows name the models each solution solves, which the solution's file never mentions. + +**3. The row says more, which the child doesn't use.** + +``` +# a LAK package's tables +1 TAB6 FILEIN lake1.tab +``` + +`ifno` says which lake the table is for. The table holds stage, volume and area, and nothing in it depends on the lake, so nothing is passed. SFR's cross-section tables are the same. A binding passes only what the child's references need. + +**4. The row says what the child's references need.** + +``` +# mfsim.nam exchanges +GWF6-GWF6 model.gwfgwf gwf_1 gwf_2 + +# model.gwfgwf exchangedata: cellidm1, cellidm2, ihc, cl1, cl2, hwva +1 5 10 1 5 1 1 50.0 50.0 100.0 +``` + +The first cellid is in `gwf_1`'s grid and the second in `gwf_2`'s, but nothing in the exchange file says which models those are. The row binding it does. So every exchange declares two [parameters](#parameters), and the binding passes them: ```toml # every exchange [params.model1] type = "model" -# sim-nam exchanges row: passes the exchange its models +[params.model2] +type = "model" + +# sim-nam exchanges row [blocks.exchanges.fields.exchanges.item.fields.exgfile] args = { model1 = "exgmnamea", model2 = "exgmnameb" } +``` + +The cellids then say which parameter they're in: `cellidm1` has [`model`](#model) `"model1"`, and `cellidm2` has `"model2"`. + +**5. The parent passes on what it was passed.** + +``` +# model.gwfgwf options +GNC6 FILEIN model.gnc +MVR6 FILEIN model.mvr -# gwf-mvr, gwf-gnc: the same parameters, passed down by exg-gwfgwf +# model.mvr period: mname1, pname1, id1, mname2, pname2, id2, mvrtype, value +gwf_1 WEL-1 1 gwf_2 LAK-1 1 FACTOR 1.0 +``` + +GNC's cellids and the mover's model names are in the exchange's models, which only the exchange's binding row says. The exchange passes its own parameters on: + +```toml +# exg-gwfgwf [blocks.options.fields.mvr_filerecord.fields.mvr6_filename] args = { model1 = "model1", model2 = "model2" } +``` + +MVT and MVE, bound the same way in transport exchanges, refer to no model, so they're passed nothing. + +**6. One component, bound under different parents.** GNC and MVR can also be a model's packages: + +``` +# a GWF model name file's packages +GNC6 model.gnc +MVR6 model.mvr +``` + +A model name file passes them nothing, so their parameters are unset, which means null. Whatever names a parameter says what null means: a GNC whose cellids' models are null reads them all in its own model, and a mover whose `model2` is null has no `MODELNAMES` and no model columns. + +**7. The row names it by reference.** + +``` +# a GWT model's SSM fileinput +WEL-1 SPC6 FILEIN wel.spc + +# wel.spc period: bndno, spcsetting +1 CONCENTRATION 10.0 +``` +SPC's `bndno` numbers the bounds of `WEL-1`. The argument is the row's `pname`, and the parameter is what it references, the package, not the string: + +```toml # utl-spc [params.target] type = "package" @@ -1057,25 +1198,64 @@ rule = "no_advanced_transport" args = { target = "pname" } ``` +`WEL-1` is a package of the flow model, not of the transport model that SSM is in, so `pname`'s `fk` has the rule `flow_model`, which case 8 explains. + +**8. No binding says it.** + +``` +# a GWT model's SSM sources +WEL-1 AUX CONCENTRATION +``` + +`WEL-1` is a package of the flow model that a `GWF6-GWT6` exchange pairs with this transport model. That's said by an exchange row naming the transport model, and an exchange binds neither SSM nor any of its ancestors, so no parameter can carry it. A [rule](#rules) does: `flow_model`. With flows read from a budget file there is no exchange, and no check. + +#### Parameters + +A component's **parameters** are what its references need from its binding (cases 4 to 7). Binding works like a function call: + +| Binding | Function call | +|---|---| +| the component | the function | +| `params` | its signature | +| the binding field | a call site | +| `args` | the arguments | +| the parent | `self` | +| an unset parameter | an argument left at its default, `None` | + +In Python terms, for GNC: + +```python +class Gnc: + def __init__(self, model1=None, model2=None): ... + +Gnc(model1=self.model1, model2=self.model2) # in an exchange: passes on its own +Gnc() # in a model name file +``` + +A component declares its parameters in [`params`](#params): + - `type` is what the parameter names, a selector as in [`parent`](#parent). - `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). -An argument names something in the binder's scope: a column of the binding row, following its `fk` to an instance, or one of the binder's own parameters. It must name exactly one, so nothing is searched. The scoping is lexical: a binder can pass only what's in its own scope, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. +A binding passes them in its `file` field's [`args`](#args). An argument names one of two things, in the parent's context: -Following an argument's `fk` applies all of it, [rule](#rules) included, and the parameter's own `rule` applies after that. SSM's `pname` has `rule = "flow_model"`, so the `target` it passes is a package of the paired flow model; `utl-spc`'s `rule = "no_advanced_transport"` then narrows it to one with no advanced transport package. +- a column of the binding row with an `fk`, and the argument is what the `fk` references (cases 4 and 7); +- one of the parent's own parameters (case 5). -A binder that passes no argument leaves the parameter unset, which is null, as for a field. Nothing fills it in from elsewhere. What null means is said by whatever names the parameter (below): a `when` naming it is off, and a cellid whose `model` names it is in the component's own model. So a mover under a model has no `MODELNAMES` or model names, and a GNC under a model reads all its cellids in that model. +It must name exactly one, so nothing is searched. Context is lexical, as in most programming languages: a parent can pass only what's in its own context, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. -The binder itself isn't a parameter. A file's owner is already known: it's what cross-component `fk`s resolve against (see [Primary and foreign keys](#primary-and-foreign-keys)), as a method's `self` is. So `utl-obs`, whose target is whatever binds it, declares none. +Following an argument's `fk` applies all of it, [rule](#rules) included, and the parameter's own `rule` applies after that. SSM's `pname` has `rule = "flow_model"`, so the `target` it passes is a package of the paired flow model, and `utl-spc`'s `rule = "no_advanced_transport"` then narrows it to one with no advanced transport package. -Three things can name a parameter: +A binding that passes no argument leaves the parameter unset, which is null, as for a field. Nothing fills it in from elsewhere. What null means is said by whatever names the parameter, which is one of: - an `fk`'s `param`. Alone, the value names the instance a parameter is set to, one of several if it's a list: `gwf-mvr`'s `mname1` is `fk = { param = ["model1", "model2"] }`, one of the exchange's two models. With a `key`, the parameter picks the instance whose key set is meant: `utl-spc`'s `bndno` is `fk = { key = "bound", param = "target" }`. -- a cellid's [`model`](#model): `"model1"` or `"model2"`. -- a field's [`when`](#when): `gwf-mvr`'s `MODELNAMES` has `when = "model2"`. +- a cellid's [`model`](#model): `"model1"` or `"model2"`. Null means the component's own model. +- a field's [`when`](#when): `gwf-mvr`'s `MODELNAMES` has `when = "model2"`. Null means off. A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC only to a list-based package, and array-based SPC (`utl-spca`) only to `gwf-rcha` and `gwf-evta` (`TspSpc.f90:353-375`). `utl-spca` says so with `type = ["gwf-rcha", "gwf-evta"]`, and `utl-spc` says so through `key = "bound"`, which RCHA and EVTA don't declare. +Two things aren't parameters. The parent is already in context, as `self` is, so `utl-obs` declares none. And what no binding says (case 8) can't be passed, so it's a rule. + In relational terms, an argument is a foreign key on the binding row, and the parameter is that column carried onto the child's row: | DFN | SQL | @@ -1089,37 +1269,11 @@ A validator checks that: - each parameter's `type` admits some component; - each argument is a parameter of every component its `file` can bind; -- each argument names exactly one of a binding row column, with an `fk`, and a binder's parameter, and can name something of the parameter's type; +- each argument names exactly one of a binding row column, with an `fk`, and a parent's parameter, and can name something of the parameter's type; - each `fk`, cellid `model` and `when` naming a parameter names one the component declares, and a cellid's is of a model. So an exchange's cellid always resolves, and so does a mover's model name. -#### Rules - -Some constraints aren't about a binding, so no [parameter](#parameters) says them. A transport model's SSM names packages of whichever flow model an exchange pairs it with, which nothing that binds SSM supplies: it comes from an exchange naming the transport model, or from a budget file. A mover's packages must have `MOVER` set. Instead, the `fk` or parameter names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. - -A rule names a subset of the target, and the value must be in it. In relational terms, the `fk` references a named view rather than the target table. A rule may stand in for a scope, or narrow a scoped target further: - -```toml -# gwt-ssm / gwe-ssm sources.pname and fileinput.pname -pk = true -fk = { path = "gwf-nam.packages.pname", rule = "flow_model" } - -# gwf-mvr packages.pname: the scope picks the model, the rule narrows its packages to movers -pk = { scope = "mname" } -fk = { path = "gwf-nam.packages.pname", scope = "mname", rule = "mover_enabled" } -``` - -The rest of the `fk` is checked as usual, by its scope or as if unscoped: its path must name a `pk` (or string array), and its `key` must be declared by some component. A consumer that doesn't implement a rule skips the check; the rest of the `fk` still says what kind of thing the value names. - -| rule | meaning | MODFLOW 6 | -|---|---|---| -| `flow_model` | in the flow model that a GWF-GWT or GWF-GWE exchange pairs with this transport model (its `exgmnamea`, where the transport model is `exgmnameb`). With flows read from a budget file there is no such exchange, and the reference isn't checked. | `exg-gwfgwt.f90:124-129`, `exg-gwfgwe.f90:121-126`, `tsp-ssm.f90:800-811` | -| `no_advanced_transport` | a flow package with no advanced transport package paired with it. SSM doesn't apply an SPC file bound to one that has, so the file isn't checked. On `utl-spc`'s and `utl-spca`'s `target` | `tsp-ssm.f90:195` | -| `mover_enabled` | a package that plays `mover_provider` or `mover_receiver`: one whose mover key sets are switched on by `MOVER` (see [Key sets](#key-sets)) | `gwf-mvr.f90:942-971` | - -The names are fixed by the schema, so a misspelled rule is an error. A new rule is added to the schema and this table together. - ## Memory catalog Each component may declare a `memory` catalog describing runtime variables accessible via the MODFLOW API. Memory variables correspond to values in the MODFLOW 6 memory manager that can be read or written at runtime using hierarchical `/`-delimited memory addresses. 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 44d8f4e6..ffc5fc0b 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1821,15 +1821,15 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B } -# Binding parameters (see `v2.Param`): what a component needs from whatever -# binds its file, beyond the binder itself, which v1 says only in prose. The -# component declares them, like a function's signature; the binding file -# passes them (see `_ARGS`). +# Binding parameters (see `v2.Param`): what a component's references need +# from its binding, beyond its parent, which v1 says only in prose. The +# component declares them, like a function's signature; the binding passes +# them (see `_ARGS`). # # - Every exchange (`exg-*`) has two models, named on `sim-nam`'s # `exchanges` row, not in its own file. Its cellids name them. # - An exchange's mover and GNC get them from the exchange. Under a model -# they're unset, meaning that model. +# they're unset (null), which a cellid reads as that model. # - SPC and SPCA apply to the flow package SSM's `fileinput` row names # (`TspSpc.f90:326-349`), skipped if an advanced transport package is # paired with it (`tsp-ssm.f90:195`). MF6 rejects array SPC on any but @@ -1845,9 +1845,9 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B "utl-spca": {"target": v2.Param(type=["gwf-rcha", "gwf-evta"], rule=_SPC_TARGET)}, } -# The arguments each binding file passes (see `v2.File.args`), as (block, -# file field, args): a column of the binding row, or the binder's own -# parameter, passed down. +# The arguments each binding passes (see `v2.File.args`), as (block, file +# field, args): a column of the binding row, or the parent's own parameter, +# passed on. _PASS_MODELS = {"model1": "model1", "model2": "model2"} _ARGS: dict[str, list[tuple[str, str, dict[str, str]]]] = { "sim-nam": [("exchanges", "exgfile", {"model1": "exgmnamea", "model2": "exgmnameb"})], diff --git a/modflow_devtools/dfns/schema.json b/modflow_devtools/dfns/schema.json index c2511a24..83f5b1c2 100644 --- a/modflow_devtools/dfns/schema.json +++ b/modflow_devtools/dfns/schema.json @@ -2090,7 +2090,7 @@ }, "Param": { "additionalProperties": false, - "description": "A binding parameter: something a component needs from whatever binds its\nfile, beyond the binder itself (see `docs/md/dfnspec.md`, \"Parameters\").\nDeclared in the component's `params`, like a function's signature, and\npassed by the binding file's `args` (see `File`). Named by an fk's\n`param`, a cellid's `model`, or a `when`.\n\n- `type`: what the parameter names, a selector as in `parent`.\n- `rule`: as on an fk (see `RULES`), a named subset of the target.\n\nA binder that passes no argument leaves it unset. Relationally, a\nparameter is a column on the child's row, with a foreign key.", + "description": "A binding parameter: what a component's references need from its\nbinding, beyond its parent (see `docs/md/dfnspec.md`, \"Bindings\").\nDeclared in the component's `params`, like a function's signature, and\npassed by the binding's `args` (see `File`). Named by an fk's\n`param`, a cellid's `model`, or a `when`.\n\n- `type`: what the parameter names, a selector as in `parent`.\n- `rule`: as on an fk (see `RULES`), a named subset of the target.\n\nA binding that passes no argument leaves it unset, i.e. null. Relationally, a\nparameter is a column on the child's row, with a foreign key.", "properties": { "type": { "anyOf": [ diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index caefe0b9..ecee8dce 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -411,16 +411,16 @@ def bound(self) -> str | None: class Param(BaseModel): """ - A binding parameter: something a component needs from whatever binds its - file, beyond the binder itself (see `docs/md/dfnspec.md`, "Parameters"). + A binding parameter: what a component's references need from its + binding, beyond its parent (see `docs/md/dfnspec.md`, "Bindings"). Declared in the component's `params`, like a function's signature, and - passed by the binding file's `args` (see `File`). Named by an fk's + passed by the binding's `args` (see `File`). Named by an fk's `param`, a cellid's `model`, or a `when`. - `type`: what the parameter names, a selector as in `parent`. - `rule`: as on an fk (see `RULES`), a named subset of the target. - A binder that passes no argument leaves it unset. Relationally, a + A binding that passes no argument leaves it unset, i.e. null. Relationally, a parameter is a column on the child's row, with a foreign key. """ From 09a2c60eb0ac75c0d03a6144802a5ca47c5e81f9 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 11:56:24 -0800 Subject: [PATCH 06/11] docs(dfns): call a reference's reach its scope Scope here is the programming sense, and so are the spec's other uses: a dimension's scope says whose scope it's in, and a scoped key is a qualified name. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 02a04c21..165f4652 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -875,7 +875,7 @@ An unprefixed length is exact, so a DFN must mark every bound. A consumer may re #### Dimension scope -Dimensions may specify a `scope` attribute controlling which other components can inherit the dimension: +Dimensions may specify a `scope` attribute controlling which other components can inherit the dimension, i.e. whose [scope](#terms) it's in: - **`"component"`**: only visible within this component (or to subpackages that list this component as their explicit parent). The default scope. - **`"model"`**: visible to any component that can share the same model instance, as determined by component `parent` attributes. A dimension defined in component A with `scope: "model"` is visible to component B if: @@ -903,11 +903,11 @@ An `fk` names an existing row: a file in which one names no row is invalid. An i An `fk` is a hierarchical path of the form `"[component.]block.field"`, naming the `pk` field it references. The `component` segment is necessary for cross-component references; it may be omitted for within-component references. On a `string` field, the path may instead name a string array (e.g. `"options.auxiliary"`), whose entries then act as the key set: the array is in effect a one-column list whose entries are its keys. A dynamic keyword (see [`dynamic`](#dynamic)) needs this form when its keywords are a string array's entries. -A cross-component `fk` resolves in the file's [context](#terms): against a singleton component (e.g. `sim-nam`), or an ancestor of the instance the file is input for (e.g. a reference from a model's package to that model's name file). +A cross-component `fk` resolves in the file's [scope](#terms): against a singleton component (e.g. `sim-nam`), or an ancestor of the instance the file is input for (e.g. a reference from a model's package to that model's name file). #### Scoped keys -MODFLOW names things hierarchically: a package name is unique within a model, and a model name within the simulation. A key can carry a **scope**, the name of another column in the same record that qualifies it, or of a record enclosing it within the same list item (a union arm's column may be scoped by the list item's). The scope belongs to the key, so it's written inside `pk` or `fk`: +MODFLOW names things hierarchically: a package name is unique within a model, and a model name within the simulation. A scoped key is a qualified name, like `mname1::pname1` in C++, whose `::` is called the scope resolution operator: `pname1` is resolved in the [scope](#terms) of model `mname1`. A key can carry a **scope**, the name of another column in the same record that qualifies it, or of a record enclosing it within the same list item (a union arm's column may be scoped by the list item's). The scope belongs to the key, so it's written inside `pk` or `fk`: ```toml pk = { scope = "mname" } @@ -918,7 +918,7 @@ fk = { path = "packages.pname", scope = "mname1" } - An `fk` with scope `s` takes the target key's qualifier from `s`. - Within a component (`"block.field"`), the target `pk` must be scoped too, and `s` matches the target row's scope column. If both scope columns have an `fk`, it must be the same. - Across components (`"component.block.field"`), `s` picks the target component's *instance*. `s` must name that instance: it must be an `fk` to the key of the row that binds that component in its parent, i.e. a list whose item has a `file` naming the component (see [`component`](#component)), either directly or through other `fk`s. For example, `gwf-mvr`'s `pname1` names a `packages` row, whose `pname` names a row of a model name file's `packages`. -- An absent scope value means the current one, as a relative path resolves against the current directory. For example, `gwf-mvr`'s `packages` rows name a package within a model, `mname`, which is given only in an exchange's mover; in a model's own mover it's absent and means that model. Like an optional `pk` given a default, the key is complete once the default is applied. +- An absent scope value means the current scope, as a relative path resolves against the current directory. For example, `gwf-mvr`'s `packages` rows name a package within a model, `mname`, which is given only in an exchange's mover; in a model's own mover it's absent and means that model. Like an optional `pk` given a default, the key is complete once the default is applied. - A scope column's own key may be scoped, forming a chain, but not a cycle. - If a column's `pk` and `fk` are both scoped, the scope is the same column. - A scope names the nearest column of that name: in the key's own record, then in each enclosing record of the same list item. @@ -1065,7 +1065,7 @@ A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model nam - The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. - The **binding row** is the record holding the binding field: here, the whole `GWF6 model.nam gwf_1` line. Its other columns may say something about the child, such as its name. - A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. A binding names a file, and so builds the tree. A reference names something in the tree, and is resolved once the tree is built. -- A reference resolves in its component's **context**: the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. Context is what programming languages call scope. This spec says context because it already uses scope for [scoped keys](#scoped-keys) and [dimension scope](#dimension-scope). +- A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. So a component's file says what it says, and its binding row may say more about it. The question in each case is whether the file's references need any of that. @@ -1237,12 +1237,12 @@ A component declares its parameters in [`params`](#params): - `type` is what the parameter names, a selector as in [`parent`](#parent). - `rule` is optional, and means what it means on an `fk`: a named subset of the target (see [Rules](#rules)). -A binding passes them in its `file` field's [`args`](#args). An argument names one of two things, in the parent's context: +A binding passes them in its `file` field's [`args`](#args). An argument names one of two things, in the parent's scope: - a column of the binding row with an `fk`, and the argument is what the `fk` references (cases 4 and 7); - one of the parent's own parameters (case 5). -It must name exactly one, so nothing is searched. Context is lexical, as in most programming languages: a parent can pass only what's in its own context, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. +It must name exactly one, so nothing is searched. Scope is lexical, as in most programming languages: a parent can pass only what's in its own scope, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. Following an argument's `fk` applies all of it, [rule](#rules) included, and the parameter's own `rule` applies after that. SSM's `pname` has `rule = "flow_model"`, so the `target` it passes is a package of the paired flow model, and `utl-spc`'s `rule = "no_advanced_transport"` then narrows it to one with no advanced transport package. @@ -1254,7 +1254,7 @@ A binding that passes no argument leaves the parameter unset, which is null, as A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC only to a list-based package, and array-based SPC (`utl-spca`) only to `gwf-rcha` and `gwf-evta` (`TspSpc.f90:353-375`). `utl-spca` says so with `type = ["gwf-rcha", "gwf-evta"]`, and `utl-spc` says so through `key = "bound"`, which RCHA and EVTA don't declare. -Two things aren't parameters. The parent is already in context, as `self` is, so `utl-obs` declares none. And what no binding says (case 8) can't be passed, so it's a rule. +Two things aren't parameters. The parent is already in scope, as `self` is, so `utl-obs` declares none. And what no binding says (case 8) can't be passed, so it's a rule. In relational terms, an argument is a foreign key on the binding row, and the parameter is that column carried onto the child's row: From 3b1cc9f2d6075214b700cec57ad18d73cc72398f Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 12:37:02 -0800 Subject: [PATCH 07/11] docs(dfns): put bindings before references Bindings build the tree that references resolve in, so the Bindings section now comes before Primary and foreign keys, and Parameters, which uses keys, key sets and rules, is its own section after Rules. Terms says how a binding differs from a reference, with the exchanges row as one that holds both. The parent and component attributes now say what a binding is. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 328 +++++++++++++++++++++++---------------------- 1 file changed, 166 insertions(+), 162 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 165f4652..a552c819 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -94,14 +94,14 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [Dimension sources](#dimension-sources) - [Bounds](#bounds) - [Dimension scope](#dimension-scope) + - [Bindings](#bindings) + - [Terms](#terms) + - [What a binding carries](#what-a-binding-carries) - [Primary/foreign keys](#primary-and-foreign-keys) - [Scoped keys](#scoped-keys) - [Key sets](#key-sets) - [Rules](#rules) - - [Bindings](#bindings) - - [Terms](#terms) - - [What a binding carries](#what-a-binding-carries) - - [Parameters](#parameters) + - [Parameters](#parameters) - [Memory catalog](#memory-catalog) - [Attributes](#attributes-1) - [`type`](#type-2) @@ -161,7 +161,7 @@ The component's type. Required. One of: #### `parent` -Except for the simulation, which is the root of the runtime hierarchy, all MF6 components have a parent. Parent-child relations may range from fully constrained (e.g., a `gwf-chd` package must be a child of a `gwf-nam` model) to completely unconstrained. Components which may be attached to multiple possible parents are historically called **subpackages**. +Except for the simulation, which is the root of the hierarchy, all MF6 components have a parent. The hierarchy is built by [bindings](#bindings): a `file` field with a [`component`](#component) makes the file it names a child of the file it's in. Parent-child relations may range from fully constrained (e.g., a `gwf-chd` package must be a child of a `gwf-nam` model) to completely unconstrained. Components which may be attached to multiple possible parents are historically called **subpackages**. Parent relationships are defined bottom-up with attribute `parent`: @@ -562,7 +562,7 @@ If a file option is introduced by one or more leading keywords (e.g. `CROSS_SECT ###### `component` -`string | [string] | null (default: null)`. The component the file is input for. Valid for components whose `parent` admits this one. Only valid when `direction` is `"in"`. +`string | [string] | null (default: null)`. The component the file is input for. A `file` field with a `component` is a [binding](#bindings): it makes the file it names a child of the file it's in. Valid for components whose `parent` admits this one. Only valid when `direction` is `"in"`. ###### `component_ftype` @@ -889,8 +889,166 @@ Examples: Only a dimension that every component in the model (or simulation) shares should be widened beyond `"component"`: in practice, the discretization's grid dims and TDIS's `nper`. Other package dimensions (`maxbound`, `nseg`, `numgnc`, `nexg`, ...) describe a single instance of their component, since two EVT packages in a model can have different `nseg`, so they keep the default `"component"` scope even when the component's parent is a model or the simulation. +### Bindings + +A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model name files, the exchanges and the solutions; a model name file names its packages; a package's options name its utility files, such as time series and observations. This section says how the DFNs describe that tree, and what a file can know about where it sits in it. It comes before [keys](#primary-and-foreign-keys) and [parameters](#parameters) because they resolve in the tree that bindings build. + +#### Terms + +- A **binding** is a `file` field with a [`component`](#component), and each value of it: a file named as the input of an instance of the component. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. +- The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. +- The **binding row** is the record holding the binding field: here, the whole `GWF6 model.nam gwf_1` line. Its other columns may say something about the child, such as its name. +- A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. +- A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. + +A binding and a reference are different things. A binding names a file, and so builds the tree: the file becomes a child of the file the binding is in. A reference names something in the tree, and is resolved once the tree is built. One row can hold both. `sim-nam`'s `exchanges` row binds the exchange file with `exgfile`, and refers to the exchange's two models with `exgmnamea` and `exgmnameb`, which are `fk`s to `sim-nam`'s `models`. + +So a component's file says what it says, and its binding row may say more about it. The question in each case is whether the file's references need any of that. + +#### What a binding carries + +Every binding in MODFLOW 6 is one of these cases. + +| | Case | Example | The child needs | Said with | +|---|---|---|---|---| +| 1 | A file, and nothing else | `OBS6 FILEIN chd.obs` | nothing | [`component`](#component) | +| 2 | The row names the child | `CHD6 model.chd CHD-1` | nothing | a `pk` on the row | +| 3 | The row says more, which the child doesn't use | `1 TAB6 FILEIN lake1.tab` | nothing | nothing | +| 4 | The row says what the child's references need | `GWF6-GWF6 model.gwfgwf gwf_1 gwf_2` | the exchange's models | `params`, `args` | +| 5 | The parent passes on what it was passed | `GNC6 FILEIN model.gnc` in an exchange | the exchange's models | `args` naming a parameter | +| 6 | One component, bound under different parents | `GNC6 model.gnc` in a model name file | nothing | no `args`: unset | +| 7 | The row names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | +| 8 | No binding says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | + +**1. A file, and nothing else.** Most bindings name a file and its component, and that's all the child needs. Its references resolve against its parent: + +``` +# a CHD package's options +TS6 FILEIN chd.ts +OBS6 FILEIN chd.obs + +# chd.obs +BEGIN continuous FILEOUT chd.obs.csv + q-west chd west +END continuous +``` + +`west` names a CHD boundary, as CHD's [`[observations]`](#observations) declares, and an observation by cellid is in the CHD's model's grid. `utl-obs` is bound by 52 components and needs nothing from any of them: whatever binds it is what it observes. References can also point down the tree: a CHD head given as a time series name names a series in the TS file it binds. + +**2. The row names the child.** A name file's row gives the child its name: + +``` +# a GWF model name file's packages +CHD6 model.chd CHD-1 +``` + +`pname` is a key of the row (`pk`), and other files refer to the package by it, within its model: a mover's `packages`, a transport model's SSM. The name is the parent's data, though, and the CHD file never refers to itself, so the child needs nothing. `mfsim.nam`'s `models` rows name models the same way, and its `solutiongroup` rows name the models each solution solves, which the solution's file never mentions. + +**3. The row says more, which the child doesn't use.** + +``` +# a LAK package's tables +1 TAB6 FILEIN lake1.tab +``` + +`ifno` says which lake the table is for. The table holds stage, volume and area, and nothing in it depends on the lake, so nothing is passed. SFR's cross-section tables are the same. A binding passes only what the child's references need. + +**4. The row says what the child's references need.** + +``` +# mfsim.nam exchanges +GWF6-GWF6 model.gwfgwf gwf_1 gwf_2 + +# model.gwfgwf exchangedata: cellidm1, cellidm2, ihc, cl1, cl2, hwva +1 5 10 1 5 1 1 50.0 50.0 100.0 +``` + +The first cellid is in `gwf_1`'s grid and the second in `gwf_2`'s, but nothing in the exchange file says which models those are. The row binding it does, with `exgmnamea` and `exgmnameb`, which are references to `sim-nam`'s `models`. So every exchange declares two [parameters](#parameters), and the binding passes them: + +```toml +# every exchange +[params.model1] +type = "model" + +[params.model2] +type = "model" + +# sim-nam exchanges row +[blocks.exchanges.fields.exchanges.item.fields.exgfile] +args = { model1 = "exgmnamea", model2 = "exgmnameb" } +``` + +The cellids then say which parameter they're in: `cellidm1` has [`model`](#model) `"model1"`, and `cellidm2` has `"model2"`. + +**5. The parent passes on what it was passed.** + +``` +# model.gwfgwf options +GNC6 FILEIN model.gnc +MVR6 FILEIN model.mvr + +# model.mvr period: mname1, pname1, id1, mname2, pname2, id2, mvrtype, value +gwf_1 WEL-1 1 gwf_2 LAK-1 1 FACTOR 1.0 +``` + +GNC's cellids and the mover's model names are in the exchange's models, which only the exchange's binding row says. The exchange passes its own parameters on: + +```toml +# exg-gwfgwf +[blocks.options.fields.mvr_filerecord.fields.mvr6_filename] +args = { model1 = "model1", model2 = "model2" } +``` + +MVT and MVE, bound the same way in transport exchanges, refer to no model, so they're passed nothing. + +**6. One component, bound under different parents.** GNC and MVR can also be a model's packages: + +``` +# a GWF model name file's packages +GNC6 model.gnc +MVR6 model.mvr +``` + +A model name file passes them nothing, so their parameters are unset, which means null. Whatever names a parameter says what null means: a GNC whose cellids' models are null reads them all in its own model, and a mover whose `model2` is null has no `MODELNAMES` and no model columns. + +**7. The row names it by reference.** + +``` +# a GWT model's SSM fileinput +WEL-1 SPC6 FILEIN wel.spc + +# wel.spc period: bndno, spcsetting +1 CONCENTRATION 10.0 +``` + +SPC's `bndno` numbers the bounds of `WEL-1`. The argument is the row's `pname`, and the parameter is what it references, the package, not the string: + +```toml +# utl-spc +[params.target] +type = "package" +rule = "no_advanced_transport" + +# gwt-ssm, gwe-ssm fileinput row +[blocks.fileinput.fields.fileinput.item.fields.spc6_filename] +args = { target = "pname" } +``` + +`WEL-1` is a package of the flow model, not of the transport model that SSM is in, so `pname`'s `fk` has the rule `flow_model`, which case 8 explains. + +**8. No binding says it.** + +``` +# a GWT model's SSM sources +WEL-1 AUX CONCENTRATION +``` + +`WEL-1` is a package of the flow model that a `GWF6-GWT6` exchange pairs with this transport model. That's said by an exchange row naming the transport model, and an exchange binds neither SSM nor any of its ancestors, so no parameter can carry it. A [rule](#rules) does: `flow_model`. With flows read from a budget file there is no exchange, and no check. + ### Primary and foreign keys +A reference names something in the tree that [bindings](#bindings) build, and resolves in its component's [scope](#terms). This section covers references to rows and keys; [cellids](#cellid) and [time series](#time_series) are covered with their types. + Sometimes a column in one list identifies a row in another list. This can be conceptualized as a primary key (PK) / foreign key (FK) relation. Integers and strings may encode PK/FK semantics with attributes `pk` and `fk`. A column referencing a grid cell instead of another list's row is a distinct concern, handled by the [`cellid`](#cellid) type rather than `pk`/`fk` — grid cells are resolved against the grid (DIS/DISV/DISU) the column refers to at runtime, not looked up via a `pk` column. **Note**: `pk` is only valid on integer and string fields appearing as columns in a tabular (i.e. regular) list's record item type. `fk` is valid there too, and additionally on integer- and string-`dtype` `Array` fields (see [Array](#array)) — there each element references another list's row, per grid cell for a grid array, or several per row for an array column in a list item; a string array's entries are keys. @@ -1055,163 +1213,9 @@ The rest of the `fk` is checked as usual, by its scope or as if unscoped: its pa The names are fixed by the schema, so a misspelled rule is an error. A new rule is added to the schema and this table together. -### Bindings - -A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model name files, the exchanges and the solutions; a model name file names its packages; a package's options name its utility files, such as time series and observations. This section says how the DFNs describe that tree, and what a file can know about where it sits in it. - -#### Terms - -- A **binding** is a `file` field with a [`component`](#component), and each value of it: a file named as the input of an instance of the component. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. -- The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. -- The **binding row** is the record holding the binding field: here, the whole `GWF6 model.nam gwf_1` line. Its other columns may say something about the child, such as its name. -- A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. A binding names a file, and so builds the tree. A reference names something in the tree, and is resolved once the tree is built. -- A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. - -So a component's file says what it says, and its binding row may say more about it. The question in each case is whether the file's references need any of that. - -#### What a binding carries - -Every binding in MODFLOW 6 is one of these cases. - -| | Case | Example | The child needs | Said with | -|---|---|---|---|---| -| 1 | A file, and nothing else | `OBS6 FILEIN chd.obs` | nothing | [`component`](#component) | -| 2 | The row names the child | `CHD6 model.chd CHD-1` | nothing | a `pk` on the row | -| 3 | The row says more, which the child doesn't use | `1 TAB6 FILEIN lake1.tab` | nothing | nothing | -| 4 | The row says what the child's references need | `GWF6-GWF6 model.gwfgwf gwf_1 gwf_2` | the exchange's models | `params`, `args` | -| 5 | The parent passes on what it was passed | `GNC6 FILEIN model.gnc` in an exchange | the exchange's models | `args` naming a parameter | -| 6 | One component, bound under different parents | `GNC6 model.gnc` in a model name file | nothing | no `args`: unset | -| 7 | The row names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | -| 8 | No binding says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | - -**1. A file, and nothing else.** Most bindings name a file and its component, and that's all the child needs. Its references resolve against its parent: - -``` -# a CHD package's options -TS6 FILEIN chd.ts -OBS6 FILEIN chd.obs - -# chd.obs -BEGIN continuous FILEOUT chd.obs.csv - q-west chd west -END continuous -``` - -`west` names a CHD boundary, as CHD's [`[observations]`](#observations) declares, and an observation by cellid is in the CHD's model's grid. `utl-obs` is bound by 52 components and needs nothing from any of them: whatever binds it is what it observes. References can also point down the tree: a CHD head given as a time series name names a series in the TS file it binds. - -**2. The row names the child.** A name file's row gives the child its name: - -``` -# a GWF model name file's packages -CHD6 model.chd CHD-1 -``` - -`pname` is a key of the row (`pk`), and other files refer to the package by it, within its model: a mover's `packages`, a transport model's SSM. The name is the parent's data, though, and the CHD file never refers to itself, so the child needs nothing. `mfsim.nam`'s `models` rows name models the same way, and its `solutiongroup` rows name the models each solution solves, which the solution's file never mentions. - -**3. The row says more, which the child doesn't use.** - -``` -# a LAK package's tables -1 TAB6 FILEIN lake1.tab -``` - -`ifno` says which lake the table is for. The table holds stage, volume and area, and nothing in it depends on the lake, so nothing is passed. SFR's cross-section tables are the same. A binding passes only what the child's references need. - -**4. The row says what the child's references need.** - -``` -# mfsim.nam exchanges -GWF6-GWF6 model.gwfgwf gwf_1 gwf_2 - -# model.gwfgwf exchangedata: cellidm1, cellidm2, ihc, cl1, cl2, hwva -1 5 10 1 5 1 1 50.0 50.0 100.0 -``` - -The first cellid is in `gwf_1`'s grid and the second in `gwf_2`'s, but nothing in the exchange file says which models those are. The row binding it does. So every exchange declares two [parameters](#parameters), and the binding passes them: - -```toml -# every exchange -[params.model1] -type = "model" - -[params.model2] -type = "model" - -# sim-nam exchanges row -[blocks.exchanges.fields.exchanges.item.fields.exgfile] -args = { model1 = "exgmnamea", model2 = "exgmnameb" } -``` - -The cellids then say which parameter they're in: `cellidm1` has [`model`](#model) `"model1"`, and `cellidm2` has `"model2"`. - -**5. The parent passes on what it was passed.** - -``` -# model.gwfgwf options -GNC6 FILEIN model.gnc -MVR6 FILEIN model.mvr - -# model.mvr period: mname1, pname1, id1, mname2, pname2, id2, mvrtype, value -gwf_1 WEL-1 1 gwf_2 LAK-1 1 FACTOR 1.0 -``` - -GNC's cellids and the mover's model names are in the exchange's models, which only the exchange's binding row says. The exchange passes its own parameters on: - -```toml -# exg-gwfgwf -[blocks.options.fields.mvr_filerecord.fields.mvr6_filename] -args = { model1 = "model1", model2 = "model2" } -``` - -MVT and MVE, bound the same way in transport exchanges, refer to no model, so they're passed nothing. - -**6. One component, bound under different parents.** GNC and MVR can also be a model's packages: - -``` -# a GWF model name file's packages -GNC6 model.gnc -MVR6 model.mvr -``` - -A model name file passes them nothing, so their parameters are unset, which means null. Whatever names a parameter says what null means: a GNC whose cellids' models are null reads them all in its own model, and a mover whose `model2` is null has no `MODELNAMES` and no model columns. - -**7. The row names it by reference.** - -``` -# a GWT model's SSM fileinput -WEL-1 SPC6 FILEIN wel.spc - -# wel.spc period: bndno, spcsetting -1 CONCENTRATION 10.0 -``` - -SPC's `bndno` numbers the bounds of `WEL-1`. The argument is the row's `pname`, and the parameter is what it references, the package, not the string: - -```toml -# utl-spc -[params.target] -type = "package" -rule = "no_advanced_transport" - -# gwt-ssm, gwe-ssm fileinput row -[blocks.fileinput.fields.fileinput.item.fields.spc6_filename] -args = { target = "pname" } -``` - -`WEL-1` is a package of the flow model, not of the transport model that SSM is in, so `pname`'s `fk` has the rule `flow_model`, which case 8 explains. - -**8. No binding says it.** - -``` -# a GWT model's SSM sources -WEL-1 AUX CONCENTRATION -``` - -`WEL-1` is a package of the flow model that a `GWF6-GWT6` exchange pairs with this transport model. That's said by an exchange row naming the transport model, and an exchange binds neither SSM nor any of its ancestors, so no parameter can carry it. A [rule](#rules) does: `flow_model`. With flows read from a budget file there is no exchange, and no check. - -#### Parameters +### Parameters -A component's **parameters** are what its references need from its binding (cases 4 to 7). Binding works like a function call: +A component's **parameters** are what its references need from its binding: cases 4 to 7 under [What a binding carries](#what-a-binding-carries). Binding works like a function call: | Binding | Function call | |---|---| From 4484907c141b52ce4189f6e732d369df2fef82bc Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 12:59:47 -0800 Subject: [PATCH 08/11] docs(dfns): call a binding's row-mates its siblings "Binding row" read as a row whose job is binding, but the binding is one field among keys, references and data: the exchanges row also refers to its two models. An argument names a sibling of the binding with an fk. The validator's message says "sibling column" to match. Co-Authored-By: Claude Opus 5.5 --- autotest/dfns/test_schema_relations.py | 6 ++-- docs/md/dfnspec.md | 34 +++++++++---------- .../dfns/migrate_to_v2_0_0_dev2.py | 4 +-- modflow_devtools/dfns/schema.py | 13 ++++--- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/autotest/dfns/test_schema_relations.py b/autotest/dfns/test_schema_relations.py index 78634dd4..ef8e446e 100644 --- a/autotest/dfns/test_schema_relations.py +++ b/autotest/dfns/test_schema_relations.py @@ -1411,7 +1411,7 @@ def test_file_args_checked(args): def test_dfns_validate_args(): - """An exchange's models are passed from its binding row, a mover's from + """An exchange's models are passed from its `sim-nam` row, a mover's from the exchange's own, and SPC's target from SSM's row. A mover under a model gets none.""" spec = Dfns(components=_ctx_transport()) @@ -1430,12 +1430,12 @@ def test_dfns_validate_args(): ( _EXG, {"model1": "nope"}, - "arg model1='nope' must name exactly one of a binding row column and a parameter " + "arg model1='nope' must name exactly one of a sibling column and a parameter " "of sim-nam", ), (_EXG, {"model1": "exgtype"}, "arg model1='exgtype': the column has no fk to follow"), (_EXG, {"model3": "exgmnamea"}, "passes 'model3', which exg-gwfgwf doesn't declare"), - (_MVR, {"model1": "model3"}, "must name exactly one of a binding row column"), + (_MVR, {"model1": "model3"}, "must name exactly one of a sibling column"), (_MVR, {"model3": "model1"}, "passes 'model3', which gwf-mvr doesn't declare"), ], ) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index a552c819..447ac2e0 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -572,7 +572,7 @@ Each component declares its token in [`ftype`](#ftype). ###### `args` -`{string: string} (default: {})`. The arguments this [binding](#bindings) passes the target's [parameters](#parameters), by name. Each names a column of the binding row with an `fk`, i.e. a sibling field, or one of this component's own parameters, passed on. For example, `sim-nam`'s `exchanges` rows have `exgfile` with `args = { model1 = "exgmnamea", model2 = "exgmnameb" }`. Only valid with `component`. +`{string: string} (default: {})`. The arguments this [binding](#bindings) passes the target's [parameters](#parameters), by name. Each names a sibling field with an `fk`, i.e. another field of the binding's record, or one of this component's own parameters, passed on. For example, `sim-nam`'s `exchanges` rows have `exgfile` with `args = { model1 = "exgmnamea", model2 = "exgmnameb" }`. Only valid with `component`. ### Composites @@ -897,13 +897,13 @@ A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model nam - A **binding** is a `file` field with a [`component`](#component), and each value of it: a file named as the input of an instance of the component. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. - The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. -- The **binding row** is the record holding the binding field: here, the whole `GWF6 model.nam gwf_1` line. Its other columns may say something about the child, such as its name. +- A binding is one field of a record, and its **siblings** are the record's other fields: on the `GWF6 model.nam gwf_1` line, `GWF6` and `gwf_1`. Siblings may be keys, references or plain data, and may say something about the child, such as its name. - A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. - A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. A binding and a reference are different things. A binding names a file, and so builds the tree: the file becomes a child of the file the binding is in. A reference names something in the tree, and is resolved once the tree is built. One row can hold both. `sim-nam`'s `exchanges` row binds the exchange file with `exgfile`, and refers to the exchange's two models with `exgmnamea` and `exgmnameb`, which are `fk`s to `sim-nam`'s `models`. -So a component's file says what it says, and its binding row may say more about it. The question in each case is whether the file's references need any of that. +So a component's file says what it says, and the binding's siblings may say more about it. The question in each case is whether the file's references need any of that. #### What a binding carries @@ -912,12 +912,12 @@ Every binding in MODFLOW 6 is one of these cases. | | Case | Example | The child needs | Said with | |---|---|---|---|---| | 1 | A file, and nothing else | `OBS6 FILEIN chd.obs` | nothing | [`component`](#component) | -| 2 | The row names the child | `CHD6 model.chd CHD-1` | nothing | a `pk` on the row | -| 3 | The row says more, which the child doesn't use | `1 TAB6 FILEIN lake1.tab` | nothing | nothing | -| 4 | The row says what the child's references need | `GWF6-GWF6 model.gwfgwf gwf_1 gwf_2` | the exchange's models | `params`, `args` | +| 2 | A sibling names the child | `CHD6 model.chd CHD-1` | nothing | a `pk` | +| 3 | A sibling says more, which the child doesn't use | `1 TAB6 FILEIN lake1.tab` | nothing | nothing | +| 4 | A sibling says what the child's references need | `GWF6-GWF6 model.gwfgwf gwf_1 gwf_2` | the exchange's models | `params`, `args` | | 5 | The parent passes on what it was passed | `GNC6 FILEIN model.gnc` in an exchange | the exchange's models | `args` naming a parameter | | 6 | One component, bound under different parents | `GNC6 model.gnc` in a model name file | nothing | no `args`: unset | -| 7 | The row names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | +| 7 | A sibling names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | | 8 | No binding says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | **1. A file, and nothing else.** Most bindings name a file and its component, and that's all the child needs. Its references resolve against its parent: @@ -935,7 +935,7 @@ END continuous `west` names a CHD boundary, as CHD's [`[observations]`](#observations) declares, and an observation by cellid is in the CHD's model's grid. `utl-obs` is bound by 52 components and needs nothing from any of them: whatever binds it is what it observes. References can also point down the tree: a CHD head given as a time series name names a series in the TS file it binds. -**2. The row names the child.** A name file's row gives the child its name: +**2. A sibling names the child.** A name file's row gives the child its name: ``` # a GWF model name file's packages @@ -944,7 +944,7 @@ CHD6 model.chd CHD-1 `pname` is a key of the row (`pk`), and other files refer to the package by it, within its model: a mover's `packages`, a transport model's SSM. The name is the parent's data, though, and the CHD file never refers to itself, so the child needs nothing. `mfsim.nam`'s `models` rows name models the same way, and its `solutiongroup` rows name the models each solution solves, which the solution's file never mentions. -**3. The row says more, which the child doesn't use.** +**3. A sibling says more, which the child doesn't use.** ``` # a LAK package's tables @@ -953,7 +953,7 @@ CHD6 model.chd CHD-1 `ifno` says which lake the table is for. The table holds stage, volume and area, and nothing in it depends on the lake, so nothing is passed. SFR's cross-section tables are the same. A binding passes only what the child's references need. -**4. The row says what the child's references need.** +**4. A sibling says what the child's references need.** ``` # mfsim.nam exchanges @@ -963,7 +963,7 @@ GWF6-GWF6 model.gwfgwf gwf_1 gwf_2 1 5 10 1 5 1 1 50.0 50.0 100.0 ``` -The first cellid is in `gwf_1`'s grid and the second in `gwf_2`'s, but nothing in the exchange file says which models those are. The row binding it does, with `exgmnamea` and `exgmnameb`, which are references to `sim-nam`'s `models`. So every exchange declares two [parameters](#parameters), and the binding passes them: +The first cellid is in `gwf_1`'s grid and the second in `gwf_2`'s, but nothing in the exchange file says which models those are. The binding's siblings do: `exgmnamea` and `exgmnameb`, which are references to `sim-nam`'s `models`. So every exchange declares two [parameters](#parameters), and the binding passes them: ```toml # every exchange @@ -991,7 +991,7 @@ MVR6 FILEIN model.mvr gwf_1 WEL-1 1 gwf_2 LAK-1 1 FACTOR 1.0 ``` -GNC's cellids and the mover's model names are in the exchange's models, which only the exchange's binding row says. The exchange passes its own parameters on: +GNC's cellids and the mover's model names are in the exchange's models, which only the exchange's row in `mfsim.nam` says. The exchange passes its own parameters on: ```toml # exg-gwfgwf @@ -1011,7 +1011,7 @@ MVR6 model.mvr A model name file passes them nothing, so their parameters are unset, which means null. Whatever names a parameter says what null means: a GNC whose cellids' models are null reads them all in its own model, and a mover whose `model2` is null has no `MODELNAMES` and no model columns. -**7. The row names it by reference.** +**7. A sibling names it by reference.** ``` # a GWT model's SSM fileinput @@ -1021,7 +1021,7 @@ WEL-1 SPC6 FILEIN wel.spc 1 CONCENTRATION 10.0 ``` -SPC's `bndno` numbers the bounds of `WEL-1`. The argument is the row's `pname`, and the parameter is what it references, the package, not the string: +SPC's `bndno` numbers the bounds of `WEL-1`. The argument is the binding's sibling `pname`, and the parameter is what it references, the package, not the string: ```toml # utl-spc @@ -1243,7 +1243,7 @@ A component declares its parameters in [`params`](#params): A binding passes them in its `file` field's [`args`](#args). An argument names one of two things, in the parent's scope: -- a column of the binding row with an `fk`, and the argument is what the `fk` references (cases 4 and 7); +- a sibling of the binding with an `fk`, and the argument is what the `fk` references (cases 4 and 7); - one of the parent's own parameters (case 5). It must name exactly one, so nothing is searched. Scope is lexical, as in most programming languages: a parent can pass only what's in its own scope, so an exchange's models reach its mover by being passed twice, from `sim-nam` to the exchange and from the exchange to the mover, and no component looks up the tree for a name. @@ -1260,7 +1260,7 @@ A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC Two things aren't parameters. The parent is already in scope, as `self` is, so `utl-obs` declares none. And what no binding says (case 8) can't be passed, so it's a rule. -In relational terms, an argument is a foreign key on the binding row, and the parameter is that column carried onto the child's row: +In relational terms, an argument is a foreign key in the binding's record, and the parameter is that column carried onto the child's row: | DFN | SQL | |---|---| @@ -1273,7 +1273,7 @@ A validator checks that: - each parameter's `type` admits some component; - each argument is a parameter of every component its `file` can bind; -- each argument names exactly one of a binding row column, with an `fk`, and a parent's parameter, and can name something of the parameter's type; +- each argument names exactly one of a sibling, with an `fk`, and a parent's parameter, and can name something of the parameter's type; - each `fk`, cellid `model` and `when` naming a parameter names one the component declares, and a cellid's is of a model. So an exchange's cellid always resolves, and so does a mover's model name. 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 ffc5fc0b..172abdda 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1846,7 +1846,7 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B } # The arguments each binding passes (see `v2.File.args`), as (block, file -# field, args): a column of the binding row, or the parent's own parameter, +# field, args): a sibling of the file field with an fk, or the parent's own parameter, # passed on. _PASS_MODELS = {"model1": "model1", "model2": "model2"} _ARGS: dict[str, list[tuple[str, str, dict[str, str]]]] = { @@ -1926,7 +1926,7 @@ def _ssm_pname_keyed(blocks: dict[str, v2.Block], block_name: str) -> bool: def _fix_spc_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: """ SPC's `bndno` names a bound of the flow package its SSM `fileinput` row - (its binding row) names, its `target` parameter (see `_PARAMS`), so + names, its `target` parameter (see `_PARAMS`), so `fk = {key = "bound", param = "target"}`, keeping its `pk` (one row per bound per period). MF6 reads `bndno` against SPC's own `maxbound` and requires that to be at least the flow package's bound count diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index ecee8dce..84b52650 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -556,7 +556,7 @@ class File(InputFieldBase): # picking one family among the selector's matches. See `Dfns.ftype_family`. component_ftype: str | None = None # The arguments this binding passes the target's parameters (see `Param`), - # by name: each a column of the binding row (`exgmnamea`), or a parameter + # by name: each a sibling field with an fk (`exgmnamea`), or a parameter # of this component, passed down (an exchange's `model1`). See # `_validate_params`. args: dict[str, str] | None = None @@ -1957,8 +1957,8 @@ def _named_components( The components whose instance a column's value can name: whatever its fk's target column names, following fks across components (MVR's `pname1` names a `packages` row, whose `pname` names a package in a - model), or, for a column without an fk that is the pk of a binding row, - the components that row binds (a name file's `pname`). + model), or, for a column without an fk that is a pk and a sibling of a + binding, the components that binding binds (a name file's `pname`). """ if (owner.name, column.name) in seen: return set() @@ -2343,9 +2343,8 @@ def _validate_args(component: "ComponentBase", spec: "Dfns") -> None: - each component the File can bind (see `Dfns.children`) declares each parameter passed; - - each argument names exactly one of a column of the binding row (the - File's siblings) and a parameter of this component, so nothing is - searched; + - each argument names exactly one of a sibling of the File and a + parameter of this component, so nothing is searched; - a column has an fk, and can name something of the parameter's type (see `_named_components`); a parameter's type can name something of the parameter's type. @@ -2375,7 +2374,7 @@ def _validate_args(component: "ComponentBase", spec: "Dfns") -> None: is_param = value in (component.params or {}) if (column is None) == (not is_param): raise ValueError( - f"{at} must name exactly one of a binding row column and a parameter " + f"{at} must name exactly one of a sibling column and a parameter " f"of {component.name}" ) if is_param: From 98e30bdcdbc091a90c24b964b0af8759850c545c Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 13:20:05 -0800 Subject: [PATCH 09/11] docs(dfns): say where case 8's information is The spec said no binding knows a transport model's flow model, since no exchange binds SSM. But mfsim.nam binds the transport model and holds the pairing, in an exchanges row rather than the binding's siblings. An argument can name only a sibling or a parent's parameter, which is why flow_model is a rule. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 447ac2e0..b2340e06 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -918,7 +918,7 @@ Every binding in MODFLOW 6 is one of these cases. | 5 | The parent passes on what it was passed | `GNC6 FILEIN model.gnc` in an exchange | the exchange's models | `args` naming a parameter | | 6 | One component, bound under different parents | `GNC6 model.gnc` in a model name file | nothing | no `args`: unset | | 7 | A sibling names it by reference | `WEL-1 SPC6 FILEIN wel.spc` in SSM | the flow package `WEL-1` | `args` naming a column with an `fk` | -| 8 | No binding says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | +| 8 | Another row in the parent says it | `WEL-1 AUX CONCENTRATION` in a transport SSM's `sources` | the flow model | a [rule](#rules) | **1. A file, and nothing else.** Most bindings name a file and its component, and that's all the child needs. Its references resolve against its parent: @@ -1036,14 +1036,14 @@ args = { target = "pname" } `WEL-1` is a package of the flow model, not of the transport model that SSM is in, so `pname`'s `fk` has the rule `flow_model`, which case 8 explains. -**8. No binding says it.** +**8. Another row in the parent says it.** ``` # a GWT model's SSM sources WEL-1 AUX CONCENTRATION ``` -`WEL-1` is a package of the flow model that a `GWF6-GWT6` exchange pairs with this transport model. That's said by an exchange row naming the transport model, and an exchange binds neither SSM nor any of its ancestors, so no parameter can carry it. A [rule](#rules) does: `flow_model`. With flows read from a budget file there is no exchange, and no check. +`WEL-1` is a package of the flow model that a `GWF6-GWT6` exchange pairs with this transport model. `mfsim.nam` says so, and `mfsim.nam` binds the transport model, but not in that binding's siblings. The pairing is in an `exchanges` row, which refers back to the transport model by `exgmnameb`. An argument names only a sibling or one of the parent's parameters, so no argument can pass it. A [rule](#rules) does: `flow_model`. With flows read from a budget file there is no exchange, and no check. ### Primary and foreign keys @@ -1189,7 +1189,7 @@ An advanced package's bounds are its features or its connections. SFR and UZF ha #### Rules -Some constraints aren't about a binding, so no [parameter](#parameters) says them. A transport model's SSM names packages of whichever flow model an exchange pairs it with, which nothing that binds SSM supplies: it comes from an exchange naming the transport model, or from a budget file. A mover's packages must have `MOVER` set. Instead, the `fk` or parameter names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. +Some constraints can't be said with a path, a scope or a [parameter](#parameters). A transport model's SSM names packages of whichever flow model an exchange pairs it with. `mfsim.nam` says which, in an `exchanges` row and not among the siblings of the binding that names the transport model, so no argument can pass it (case 8 in [What a binding carries](#what-a-binding-carries)). With flows from a budget file, nothing says it. A mover's packages must have `MOVER` set. Instead, the `fk` or parameter names a **rule**: the spec states what the rule means, below, and consumers implement it in code, like JSON Schema's `format`. A rule names a subset of the target, and the value must be in it. In relational terms, the `fk` references a named view rather than the target table. A rule may stand in for a scope, or narrow a scoped target further: @@ -1258,7 +1258,7 @@ A binding that passes no argument leaves the parameter unset, which is null, as A parameter's `type` can say what a key can't. MODFLOW 6 applies list-based SPC only to a list-based package, and array-based SPC (`utl-spca`) only to `gwf-rcha` and `gwf-evta` (`TspSpc.f90:353-375`). `utl-spca` says so with `type = ["gwf-rcha", "gwf-evta"]`, and `utl-spc` says so through `key = "bound"`, which RCHA and EVTA don't declare. -Two things aren't parameters. The parent is already in scope, as `self` is, so `utl-obs` declares none. And what no binding says (case 8) can't be passed, so it's a rule. +Two things aren't parameters. The parent is already in scope, as `self` is, so `utl-obs` declares none. And what's in another row of the parent, not a sibling of the binding (case 8), can't be passed, so it's a rule. In relational terms, an argument is a foreign key in the binding's record, and the parameter is that column carried onto the child's row: From 5e09be95ea183a86eeb38a1eecbbe7eb4e231a87 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 13:27:26 -0800 Subject: [PATCH 10/11] docs(dfns): define a binding as naming a subcomponent's input A binding is a field in the parent's input naming a subcomponent's input file. It introduces what it names; a reference names something that exists without it, and resolves by lookup in the tree. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index b2340e06..f172274c 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -895,13 +895,13 @@ A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model nam #### Terms -- A **binding** is a `file` field with a [`component`](#component), and each value of it: a file named as the input of an instance of the component. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. -- The bound instance's **parent** is the instance whose file holds the binding. Bindings make the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. +- A **binding** is a field in a component's input that names a subcomponent's input file: a `file` field with a [`component`](#component), which says what the subcomponent is. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. +- The subcomponent's **parent** is the component whose input holds the binding. Bindings build the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. - A binding is one field of a record, and its **siblings** are the record's other fields: on the `GWF6 model.nam gwf_1` line, `GWF6` and `gwf_1`. Siblings may be keys, references or plain data, and may say something about the child, such as its name. -- A **reference** is a value that names something else in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. +- A **reference** is a value that names something introduced elsewhere in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. - A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. -A binding and a reference are different things. A binding names a file, and so builds the tree: the file becomes a child of the file the binding is in. A reference names something in the tree, and is resolved once the tree is built. One row can hold both. `sim-nam`'s `exchanges` row binds the exchange file with `exgfile`, and refers to the exchange's two models with `exgmnamea` and `exgmnameb`, which are `fk`s to `sim-nam`'s `models`. +A binding and a reference are different things. A binding introduces what it names: the subcomponent is in the simulation because its parent binds it. A reference names something that exists without it, and resolves by looking it up in the tree, once bindings have built it. One row can hold both. `sim-nam`'s `exchanges` row binds the exchange file with `exgfile`, and refers to the exchange's two models with `exgmnamea` and `exgmnameb`, which are `fk`s to `sim-nam`'s `models`. So a component's file says what it says, and the binding's siblings may say more about it. The question in each case is whether the file's references need any of that. From 5f4a3c19dd6e458dae945505ab428ad5b9a32899 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 11 Nov 2026 13:36:27 -0800 Subject: [PATCH 11/11] docs(dfns): match the terms to the PR, and fix scope's singletons Terms now defines parameters and arguments. Scope lists sim-tdis alone: sim-nam is every component's ancestor already, and no fk targets sim-tdis; it's in scope for its nper dimension. Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index f172274c..775107a5 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -898,8 +898,9 @@ A MODFLOW 6 simulation is a tree of input files. `mfsim.nam` names the model nam - A **binding** is a field in a component's input that names a subcomponent's input file: a `file` field with a [`component`](#component), which says what the subcomponent is. `GWF6 model.nam gwf_1` in `mfsim.nam` binds `model.nam` as a `gwf-nam`. - The subcomponent's **parent** is the component whose input holds the binding. Bindings build the simulation's tree, and the [`parent`](#parent) attribute says which component types may be the parent of which. - A binding is one field of a record, and its **siblings** are the record's other fields: on the `GWF6 model.nam gwf_1` line, `GWF6` and `gwf_1`. Siblings may be keys, references or plain data, and may say something about the child, such as its name. -- A **reference** is a value that names something introduced elsewhere in the simulation: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. -- A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, the simulation's singletons (`sim-nam`, `sim-tdis`), and the component's [parameters](#parameters), which its binding passes it. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. +- A **reference** is a value that names something introduced elsewhere in the simulation, and resolves by looking it up in the tree: a row (an [`fk`](#primary-and-foreign-keys)), a key in a [key set](#key-sets), a grid cell (a [cellid](#cellid)) or a time series. +- A component's **parameters** declare what its references need from the component that binds it, and **arguments** are what the parent passes to them, at the binding. See [Parameters](#parameters). +- A reference resolves in its component's **scope**: what it can see by name. That's the component itself, its ancestors, `sim-tdis`, and the component's [parameters](#parameters), which its binding passes it. `sim-tdis` is a child of the simulation, not an ancestor, but there's only one, so every component can see it: its `nper` is a [dimension](#dimension-scope) of every component. The spec's other uses of scope are the same idea. A [dimension's scope](#dimension-scope) says which components' scopes include it, and a [scoped key](#scoped-keys) names the scope its key is unique in, or resolves in. A binding and a reference are different things. A binding introduces what it names: the subcomponent is in the simulation because its parent binds it. A reference names something that exists without it, and resolves by looking it up in the tree, once bindings have built it. One row can hold both. `sim-nam`'s `exchanges` row binds the exchange file with `exgfile`, and refers to the exchange's two models with `exgmnamea` and `exgmnameb`, which are `fk`s to `sim-nam`'s `models`. @@ -1061,7 +1062,7 @@ An `fk` names an existing row: a file in which one names no row is invalid. An i An `fk` is a hierarchical path of the form `"[component.]block.field"`, naming the `pk` field it references. The `component` segment is necessary for cross-component references; it may be omitted for within-component references. On a `string` field, the path may instead name a string array (e.g. `"options.auxiliary"`), whose entries then act as the key set: the array is in effect a one-column list whose entries are its keys. A dynamic keyword (see [`dynamic`](#dynamic)) needs this form when its keywords are a string array's entries. -A cross-component `fk` resolves in the file's [scope](#terms): against a singleton component (e.g. `sim-nam`), or an ancestor of the instance the file is input for (e.g. a reference from a model's package to that model's name file). +A cross-component `fk` resolves in the file's [scope](#terms): against an ancestor of the instance the file is input for (e.g. `sim-nam`, or a model's name file for a reference from one of its packages), or through a [parameter](#parameters). #### Scoped keys @@ -1215,7 +1216,7 @@ The names are fixed by the schema, so a misspelled rule is an error. A new rule ### Parameters -A component's **parameters** are what its references need from its binding: cases 4 to 7 under [What a binding carries](#what-a-binding-carries). Binding works like a function call: +A component's **parameters** declare what its references need from the component that binds it: cases 4 to 7 under [What a binding carries](#what-a-binding-carries). Binding works like a function call: | Binding | Function call | |---|---|