diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.json index 06b33f00..de56a838 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.json @@ -127,7 +127,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.toml index de08bb7d..cd746510 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.toml @@ -117,6 +117,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.yaml index 85c34808..e09219c6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-chdg.yaml @@ -131,6 +131,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: constant head auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.json index aac74afd..6f88e5cb 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.json @@ -150,7 +150,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.toml index 6cae93ad..e59c5a6a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.toml @@ -140,6 +140,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.yaml index 2079d741..847c2f37 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-drng.yaml @@ -160,6 +160,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: drain auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.json index aeb96e7b..f4b5fcde 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.json @@ -169,7 +169,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.toml index 32e5578d..5a499d8d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.toml @@ -161,6 +161,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.yaml index 4b7029c5..9f35651a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-evta.yaml @@ -172,6 +172,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: evapotranspiration auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.json index 5e86cb77..732098b9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.json @@ -137,7 +137,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.toml index b2da8f02..b238dc48 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.toml @@ -127,6 +127,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.yaml index abd04c67..fa0f618a 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-ghbg.yaml @@ -142,6 +142,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: general-head boundary auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.json index 3b562664..90c02721 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.json @@ -147,7 +147,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.toml index e6f5355b..902e1755 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.toml @@ -139,6 +139,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.yaml index 7218308a..0b2c8988 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rcha.yaml @@ -156,6 +156,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: recharge auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.json index a4f98cf6..d2434383 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.json @@ -149,7 +149,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.toml index 7e148a41..2266e63d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.toml @@ -139,6 +139,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.yaml index 19186a4c..107dab03 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-rivg.yaml @@ -151,6 +151,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: river auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.json index 12e0380b..11df6370 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.json @@ -155,7 +155,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.toml index c701f388..e805d77c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.toml @@ -143,6 +143,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.yaml index 23ad18d0..43f693c3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-welg.yaml @@ -172,6 +172,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: well auxiliary variable iaux 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 2b5a496e..7daa016c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.json @@ -107,7 +107,8 @@ "type": "string", "longname": "name of model", "description": "is the user-assigned name of the model. The model name cannot exceed 16 characters and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters.", - "tagged": false + "tagged": false, + "pk": true } } } @@ -146,13 +147,15 @@ "type": "string", "longname": "name of model A", "description": "is the name of the first model that is part of this exchange.", - "tagged": false + "tagged": false, + "fk": "models.mname" }, "exgmnameb": { "type": "string", "longname": "name of model B", "description": "is the name of the second model that is part of this exchange.", - "tagged": false + "tagged": false, + "fk": "models.mname" } } } @@ -201,7 +204,8 @@ "longname": "array of model names in this solution", "description": "is the array of model names to add to this solution. The number of model names is determined by the number of model names the user provides on this line.", "tagged": false, - "dtype": "string" + "dtype": "string", + "fk": "models.mname" } } } 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 dc29485b..b85b21c7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.toml @@ -96,6 +96,7 @@ type = "string" longname = "name of model" description = "is the user-assigned name of the model. The model name cannot exceed 16 characters and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters." tagged = false +pk = true [blocks.exchanges.fields.exchanges] type = "list" @@ -129,12 +130,14 @@ type = "string" longname = "name of model A" description = "is the name of the first model that is part of this exchange." tagged = false +fk = "models.mname" [blocks.exchanges.fields.exchanges.item.fields.exgmnameb] type = "string" longname = "name of model B" description = "is the name of the second model that is part of this exchange." tagged = false +fk = "models.mname" [blocks.solutiongroup.fields.mxiter] type = "integer" @@ -178,6 +181,7 @@ longname = "array of model names in this solution" description = "is the array of model names to add to this solution. The number of model names is determined by the number of model names the user provides on this line." tagged = false dtype = "string" +fk = "models.mname" [blocks.solutiongroup.header.field] name = "group_num" 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 b1019f8e..256d0f6c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/sim-nam.yaml @@ -104,6 +104,7 @@ blocks: and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters. tagged: false + pk: true exchanges: fields: exchanges: @@ -135,11 +136,13 @@ blocks: longname: name of model A description: is the name of the first model that is part of this exchange. tagged: false + fk: models.mname exgmnameb: type: string longname: name of model B description: is the name of the second model that is part of this exchange. tagged: false + fk: models.mname solutiongroup: fields: mxiter: @@ -182,6 +185,7 @@ blocks: is determined by the number of model names the user provides on this line. tagged: false dtype: string + fk: models.mname header: field: name: group_num diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.json index 489c1691..fd93a564 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.json @@ -39,7 +39,8 @@ "type": "string", "longname": "model name", "description": "is the unique model name.", - "tagged": false + "tagged": false, + "fk": "sim-nam.models.mname" }, "mrank": { "type": "integer", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.toml index 9563efdf..cc547318 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.toml @@ -34,6 +34,7 @@ type = "string" longname = "model name" description = "is the unique model name." tagged = false +fk = "sim-nam.models.mname" [blocks.partitions.fields.partitions.item.fields.mrank] type = "integer" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.yaml index ef773394..440af81d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-hpc.yaml @@ -35,6 +35,7 @@ blocks: longname: model name description: is the unique model name. tagged: false + fk: sim-nam.models.mname mrank: type: integer longname: model rank diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.json index c4dfe5cd..f777677e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.json @@ -138,7 +138,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.toml index cab92efe..4a318f13 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.toml @@ -126,6 +126,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.yaml index ba333452..748a19cf 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-chdg.yaml @@ -138,6 +138,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: constant head auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.json index 5e129fad..67abe55c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.json @@ -161,7 +161,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.toml index e15bcb6c..f3f444da 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.toml @@ -149,6 +149,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.yaml index 077ffb5c..08a2e571 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-drng.yaml @@ -167,6 +167,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: drain auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.json index 68e06346..52fe1744 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.json @@ -180,7 +180,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.toml index 81464bc5..e4e3b8f3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.toml @@ -170,6 +170,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.yaml index e1c14720..3d53de21 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-evta.yaml @@ -179,6 +179,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: evapotranspiration auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.json index edd5bde7..e3704f72 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.json @@ -148,7 +148,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.toml index 24a2fa4e..09fa02a9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.toml @@ -136,6 +136,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.yaml index 6277a581..b7da1933 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-ghbg.yaml @@ -149,6 +149,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: general-head boundary auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.json index a11a9955..22c38bd9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.json @@ -158,7 +158,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.toml index 27deab51..7947f52b 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.toml @@ -148,6 +148,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.yaml index ece09c52..40c6f59f 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rcha.yaml @@ -163,6 +163,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: recharge auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.json index 556e4ce8..45423a36 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.json @@ -160,7 +160,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.toml index 31883dbe..57a7d7ca 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.toml @@ -148,6 +148,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.yaml index b076f925..06abe03e 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-rivg.yaml @@ -158,6 +158,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: river auxiliary variable iaux diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.json index 70dcb875..b3d76be6 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.json @@ -166,7 +166,8 @@ "longname": "auxiliary variable name", "description": "name of the auxiliary variable the following array sets; must be one of the auxiliary variables.", "tagged": false, - "fk": "options.auxiliary" + "fk": "options.auxiliary", + "dynamic": true }, "aux": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.toml index 6d31f066..ca8952e5 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.toml @@ -152,6 +152,7 @@ longname = "auxiliary variable name" description = "name of the auxiliary variable the following array sets; must be one of the auxiliary variables." tagged = false fk = "options.auxiliary" +dynamic = true [blocks.period.fields.aux.item.fields.aux] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.yaml index b70d8c65..720a352c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-welg.yaml @@ -179,6 +179,7 @@ blocks: auxiliary variables. tagged: false fk: options.auxiliary + dynamic: true aux: type: array longname: well auxiliary variable iaux 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 4b093599..b6a28aae 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.json @@ -107,7 +107,8 @@ "type": "string", "longname": "name of model", "description": "is the user-assigned name of the model. The model name cannot exceed 16 characters and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters.", - "tagged": false + "tagged": false, + "pk": true } } } @@ -146,13 +147,15 @@ "type": "string", "longname": "name of model A", "description": "is the name of the first model that is part of this exchange.", - "tagged": false + "tagged": false, + "fk": "models.mname" }, "exgmnameb": { "type": "string", "longname": "name of model B", "description": "is the name of the second model that is part of this exchange.", - "tagged": false + "tagged": false, + "fk": "models.mname" } } } @@ -201,7 +204,8 @@ "longname": "array of model names in this solution", "description": "is the array of model names to add to this solution. The number of model names is determined by the number of model names the user provides on this line.", "tagged": false, - "dtype": "string" + "dtype": "string", + "fk": "models.mname" } } } 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 9e9c7b0d..6ca9b262 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.toml @@ -96,6 +96,7 @@ type = "string" longname = "name of model" description = "is the user-assigned name of the model. The model name cannot exceed 16 characters and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters." tagged = false +pk = true [blocks.exchanges.fields.exchanges] type = "list" @@ -129,12 +130,14 @@ type = "string" longname = "name of model A" description = "is the name of the first model that is part of this exchange." tagged = false +fk = "models.mname" [blocks.exchanges.fields.exchanges.item.fields.exgmnameb] type = "string" longname = "name of model B" description = "is the name of the second model that is part of this exchange." tagged = false +fk = "models.mname" [blocks.solutiongroup.fields.mxiter] type = "integer" @@ -178,6 +181,7 @@ longname = "array of model names in this solution" description = "is the array of model names to add to this solution. The number of model names is determined by the number of model names the user provides on this line." tagged = false dtype = "string" +fk = "models.mname" [blocks.solutiongroup.header.field] name = "group_num" 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 dc47c128..33d3aaef 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/sim-nam.yaml @@ -104,6 +104,7 @@ blocks: and must not have blanks within the name. The model name is case insensitive; any lowercase letters are converted and stored as upper case letters. tagged: false + pk: true exchanges: fields: exchanges: @@ -135,11 +136,13 @@ blocks: longname: name of model A description: is the name of the first model that is part of this exchange. tagged: false + fk: models.mname exgmnameb: type: string longname: name of model B description: is the name of the second model that is part of this exchange. tagged: false + fk: models.mname solutiongroup: fields: mxiter: @@ -182,6 +185,7 @@ blocks: is determined by the number of model names the user provides on this line. tagged: false dtype: string + fk: models.mname header: field: name: group_num diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.json index 4336302d..a92aa0a3 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.json @@ -39,7 +39,8 @@ "type": "string", "longname": "model name", "description": "is the unique model name.", - "tagged": false + "tagged": false, + "fk": "sim-nam.models.mname" }, "mrank": { "type": "integer", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.toml index 8fa2ee6a..ccd6d7c8 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.toml @@ -34,6 +34,7 @@ type = "string" longname = "model name" description = "is the unique model name." tagged = false +fk = "sim-nam.models.mname" [blocks.partitions.fields.partitions.item.fields.mrank] type = "integer" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.yaml index fe7eddb9..45e4f7e7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-hpc.yaml @@ -35,6 +35,7 @@ blocks: longname: model name description: is the unique model name. tagged: false + fk: sim-nam.models.mname mrank: type: integer longname: model rank diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 437c407c..c819fa45 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -9,7 +9,7 @@ from modflow_devtools.dfn.schema import Dfn from modflow_devtools.dfns import fetch_dfns, migrate from modflow_devtools.dfns import schema as v2 -from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import to_v2_0_0_dev2 +from modflow_devtools.dfns.migrate_to_v2_0_0_dev2 import _fix_model_name_relations, to_v2_0_0_dev2 from modflow_devtools.dfns.migrate_to_v2_0_0_dev3 import to_v2_0_0_dev3 FORMATS = ["yaml", "toml", "json"] @@ -333,6 +333,33 @@ def test_migrate_period_union_element(dfn_dir, name, setting): assert item.arms["rate"].fields["outletno"].fk == "outlets.outletno" +def test_migrate_sim_nam_model_keys(dfn_dir): + component = _migrate_dev3(dfn_dir, "sim-nam") + + def item(block): + return component.blocks[block].fields[block].item + + assert item("models").fields["mname"].pk + exchanges = item("exchanges") + assert exchanges.fields["exgmnamea"].fk == "models.mname" + assert exchanges.fields["exgmnameb"].fk == "models.mname" + assert item("solutiongroup").fields["slnmnames"].fk == "models.mname" + # unchanged by a second pass + assert _fix_model_name_relations("sim-nam", component.blocks) == component.blocks + + +def test_migrate_hpc_model_names(dfn_dir): + """HPC's leading model name is a key into sim-nam's models, but data, not + a dynamic keyword, so its list stays untagged.""" + component = _migrate_dev3(dfn_dir, "utl-hpc") + partitions = component.blocks["partitions"].fields["partitions"] + mname = partitions.item.fields["mname"] + assert mname.fk == "sim-nam.models.mname" + assert not mname.pk and not mname.dynamic + assert not partitions.tagged + assert _fix_model_name_relations("utl-hpc", component.blocks) == component.blocks + + def test_migrate_auxiliary_stays_self_sizing(dfn_dir): component = _migrate_dev3(dfn_dir, "gwf-chd") assert component.blocks["options"].fields["auxiliary"].shape == [] diff --git a/autotest/dfns/test_schema.py b/autotest/dfns/test_schema.py index 703ff9fb..aa83f377 100644 --- a/autotest/dfns/test_schema.py +++ b/autotest/dfns/test_schema.py @@ -546,14 +546,16 @@ def test_block_tagged_list_item_keyword_must_be_unique(): Block(name="options", fields={"a": _ts_list("a"), "b": _ts_list("b")}) -def _aux_list(fk="options.auxiliary", tagged=False): +def _aux_list(): return List( name="aux", optional=True, item=Record( name="aux", fields={ - "auxname": String(name="auxname", tagged=tagged, fk=fk), + "auxname": String( + name="auxname", tagged=False, fk="options.auxiliary", dynamic=True + ), "aux": Array(name="aux", dtype="double", tagged=False, shape=["nodes"]), }, ), @@ -561,8 +563,8 @@ def _aux_list(fk="options.auxiliary", tagged=False): def test_list_led_by_dynamic_key_is_tagged(): - """A record led by an untagged String with a hierarchical-path fk is - keyword-led: its keywords are the fk target's entries, known at runtime.""" + """A record led by a dynamic String is keyword-led: its keywords are the + fk target's keys, known at runtime.""" lst = _aux_list() assert lst.tagged is True assert lst.item_tags == [] @@ -575,6 +577,9 @@ def test_list_led_by_dynamic_key_is_tagged(): String(name="auxname", tagged=False), String(name="auxname", tagged=False, fk="packagedata"), String(name="auxname", tagged=False, fk_ref="pname"), + # a key, but data: leading isn't enough without `dynamic` + String(name="auxname", tagged=False, fk="options.auxiliary"), + String(name="mname", tagged=False, fk="sim-nam.models.mname"), Integer(name="auxname", tagged=False, fk="options.auxiliary"), ], ) @@ -583,6 +588,31 @@ def test_list_led_by_other_untagged_value_is_untagged(auxname): assert List(name="aux", item=item).tagged is False +@pytest.mark.parametrize( + "kwargs,match", + [ + ({"fk": "options.auxiliary"}, "requires tagged=False"), + ({"tagged": False}, "requires an fk"), + ({"tagged": False, "fk_ref": "pname"}, "requires an fk"), + ({"tagged": False, "fk": "packagedata", "fk_ref": "pname"}, "requires an fk"), + ], +) +def test_dynamic_string_requires_untagged_static_fk(kwargs, match): + with pytest.raises(ValueError, match=match): + String(name="auxname", dynamic=True, **kwargs) + + +@pytest.mark.parametrize("fk", ["options.auxiliary", "packagedata", "gwf-nam.packages.pname"]) +def test_dynamic_string_accepts_any_static_fk(fk): + assert String(name="auxname", tagged=False, fk=fk, dynamic=True).dynamic + + +def test_dynamic_string_must_lead_record(): + auxname = String(name="auxname", tagged=False, fk="options.auxiliary", dynamic=True) + with pytest.raises(ValueError, match="only the first field may be dynamic"): + Record(name="aux", fields={"v": Double(name="v", tagged=False), "auxname": auxname}) + + def test_block_dynamic_key_list_may_precede_fields(): block = Block( name="period", @@ -626,7 +656,7 @@ def test_array_package_aux_is_list_of_named_arrays(dev3_spec, name, shape): assert list(aux.item.fields) == ["auxname", "aux"] auxname, array = aux.item.fields.values() assert isinstance(auxname, String) - assert not auxname.tagged + assert not auxname.tagged and auxname.dynamic assert auxname.fk == "options.auxiliary" assert isinstance(array, Array) assert not array.tagged and not array.optional diff --git a/autotest/dfns/test_schema_relations.py b/autotest/dfns/test_schema_relations.py index 1f8866f7..2413624b 100644 --- a/autotest/dfns/test_schema_relations.py +++ b/autotest/dfns/test_schema_relations.py @@ -136,7 +136,7 @@ def _aux_ctx(fk="options.auxiliary", auxiliary_dtype="string", tagged=False): """A grid-array package whose period `aux` list is keyed by an auxiliary name.""" auxiliary = Array(name="auxiliary", dtype=auxiliary_dtype, optional=True) options = Block(name="options", fields={"auxiliary": auxiliary}) - auxname = String(name="auxname", tagged=tagged, fk=fk) + auxname = String(name="auxname", tagged=tagged, fk=fk, dynamic=not tagged) array = Array(name="aux", dtype="double", tagged=False) item = Record(name="aux", fields={"auxname": auxname, "aux": array}) period = Block(name="period", fields={"aux": List(name="aux", optional=True, item=item)}) @@ -156,12 +156,12 @@ def test_dfns_validate_fk_to_string_array_tagged_string(): def test_dfns_validate_fk_to_non_string_array_rejected(): - with pytest.raises(ValueError, match="must name a string array"): + with pytest.raises(ValueError, match="is not a list block"): Dfns(components=_aux_ctx(auxiliary_dtype="double")) def test_dfns_validate_fk_to_missing_field_rejected(): - with pytest.raises(ValueError, match="must name a string array"): + with pytest.raises(ValueError, match="is not a list block"): Dfns(components=_aux_ctx(fk="options.nosuchfield")) @@ -177,14 +177,14 @@ def _union_ctx(arm): def _keyed_arm(fk): - auxname = String(name="auxname", tagged=False, fk=fk) + auxname = String(name="auxname", tagged=False, fk=fk, dynamic=True) array = Array(name="aux", dtype="double", tagged=False) return Record(name="aux", fields={"auxname": auxname, "aux": array}) def test_dfns_validate_fk_in_union_item_record_arm(): assert "gwf-test" in Dfns(components=_union_ctx(_keyed_arm("options.auxiliary"))).components - with pytest.raises(ValueError, match="must name a string array"): + with pytest.raises(ValueError, match="is not a list block"): Dfns(components=_union_ctx(_keyed_arm("options.nosuchfield"))) @@ -194,6 +194,86 @@ def test_dfns_validate_fk_in_union_item_scalar_arm(): Dfns(components=_union_ctx(arm)) +def _model_names_ctx(fk="models.mname", pk=True): + """A simulation whose solution rows each name several models, by key.""" + mname = String(name="mname", tagged=False, pk=pk) + models_item = Record(name="models", fields={"mtype": String(name="mtype"), "mname": mname}) + models = Block(name="models", fields={"models": List(name="models", item=models_item)}) + slnmnames = Array(name="slnmnames", dtype="string", tagged=False, fk=fk) + sln_item = Record( + name="sln", fields={"slntype": String(name="slntype"), "slnmnames": slnmnames} + ) + sln = Block(name="sln", fields={"sln": List(name="sln", item=sln_item)}) + return {"sim-nam": Simulation(name="sim-nam", blocks={"models": models, "sln": sln})} + + +def test_dfns_validate_string_array_fk(): + assert "sim-nam" in Dfns(components=_model_names_ctx()).components + + +def test_dfns_validate_string_array_fk_no_pk_rejected(): + with pytest.raises(ValueError, match="has no pk=True field"): + Dfns(components=_model_names_ctx(pk=False)) + + +def test_dfns_validate_string_array_fk_unknown_block_rejected(): + with pytest.raises(ValueError, match="is not a list block"): + Dfns(components=_model_names_ctx(fk="nosuchblock.mname")) + + +def _hpc_ctx(dynamic=False): + """A simulation plus an HPC-like package whose rows lead with a model name.""" + mname = String(name="mname", tagged=False, fk="sim-nam.models.mname", dynamic=dynamic) + mrank = Integer(name="mrank", tagged=False) + item = Record(name="partitions", fields={"mname": mname, "mrank": mrank}) + partitions = Block(name="partitions", fields={"partitions": List(name="partitions", item=item)}) + hpc = Package(name="utl-hpc", parent="sim-nam", blocks={"partitions": partitions}) + return {**_model_names_ctx(), "utl-hpc": hpc} + + +def test_dfns_validate_leading_fk_to_list_pk_is_data(): + """A leading untagged String keying another component's list is data + unless marked dynamic: its list stays untagged.""" + spec = Dfns(components=_hpc_ctx()) + assert spec.components["utl-hpc"].blocks["partitions"].fields["partitions"].tagged is False + + +def test_dfns_validate_dynamic_key_to_list_pk(): + """A dynamic key's keywords may come from a list's pk as well as a string array.""" + spec = Dfns(components=_hpc_ctx(dynamic=True)) + assert spec.components["utl-hpc"].blocks["partitions"].fields["partitions"].tagged is True + + +def test_dfns_validate_dynamic_key_bare_block_fk(): + """A bare block name without fk_ref is as static as a path, so a dynamic + key may use it: the keywords are that block's pk values.""" + pname = String(name="pname", tagged=False, pk=True) + packages_item = Record(name="packages", fields={"pname": pname}) + packages = Block( + name="packages", fields={"packages": List(name="packages", item=packages_item)} + ) + key = String(name="pname", tagged=False, fk="packages", dynamic=True) + item = Record(name="rate", fields={"pname": key, "rate": Double(name="rate", tagged=False)}) + period = Block(name="period", fields={"rate": List(name="rate", item=item)}) + pkg = Package( + name="gwf-test", parent="gwf-nam", blocks={"packages": packages, "period": period} + ) + spec = Dfns(components={"gwf-nam": Model(name="gwf-nam", blocks=None), "gwf-test": pkg}) + assert spec.components["gwf-test"].blocks["period"].fields["rate"].tagged is True + + +@pytest.mark.parametrize("dtype", ["integer", "string"]) +def test_array_fk_bare_block_rejected(dtype): + with pytest.raises(ValueError, match="must be a hierarchical path"): + Array(name="slnmnames", dtype=dtype, fk="models") + + +@pytest.mark.parametrize("dtype", ["double", "keyword"]) +def test_array_fk_requires_integer_or_string_dtype(dtype): + with pytest.raises(ValueError, match="fk requires dtype='integer' or 'string'"): + Array(name="slnmnames", dtype=dtype, fk="models.mname") + + 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) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 338c0b92..476f7f31 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -51,6 +51,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [`pk`](#pk) - [`fk`](#fk) - [`fk_ref`](#fk_ref) + - [`dynamic`](#dynamic) - [Integer](#integer) - [Type-specific attributes](#type-specific-attributes-3) - [`valid`](#valid-1) @@ -386,7 +387,7 @@ A list's default value is interpreted as a single element. This allows consumers `boolean (default: true)`. Whether the field begins with a keyword in input files. On a scalar or an array, it says whether the field's own name precedes its value, e.g. `MAXBOUND `. Other types derive it, so it is never declared on them: - A keyword always begins with itself, so it is always tagged. -- A record is tagged if its first field is, or is a dynamic keyword (see [List](#list)). +- A record is tagged if its first field is, or is a dynamic keyword (see [`dynamic`](#dynamic)). - A union is tagged if every arm is, so that the keyword picks the arm. This is a tagged union in the type-theory sense. An untagged union has an arm that begins with a value instead. - A list is tagged if its item is (see [List](#list)). @@ -430,12 +431,16 @@ Type `string`. ###### `fk` -`string | null (default: null)`. Marks this scalar as a foreign key. Valid only on integer or string scalars that are columns in a list item record. Two forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) bare block name (e.g., `"packagedata"`) — used together with `fk_ref` to name the block within the runtime-resolved target component, leaving only the pk field to be discovered. A string's hierarchical path may also name a string array, whose entries are then the valid values (e.g. `"options.auxiliary"`). See "Primary/foreign keys". +`string | null (default: null)`. Marks this scalar as a foreign key. Valid only on integer or string scalars that are columns in a list item record. Two forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) bare block name (e.g., `"packagedata"`) — naming a block whose `pk` field is the target: in this component when used alone, or, together with `fk_ref`, within the runtime-resolved target component. A string's hierarchical path may also name a string array, whose entries are then the valid values (e.g. `"options.auxiliary"`). See "Primary/foreign keys". ###### `fk_ref` `string | null (default: null)`. For FKs whose target component is only known at runtime. Names a sibling string field whose value identifies the target component. May be set alone (block within target also unknown) or together with `fk` as a bare block name (block known, component not). See "Primary/foreign keys". +###### `dynamic` + +`boolean (default: false)`. Marks this string as a **dynamic keyword**: its value is itself a keyword, one of the keys its `fk` names, so a record it begins is keyword-led (see [List](#list)). Requires `tagged: false` and an `fk` without `fk_ref`, and is valid only on a record's first field. The keywords must be known before the line is read, so they can't depend on a field later on the same line, as an `fk_ref` would make them. This is a fact about the input grammar, separate from `fk`, which only says what the value references. A leading untagged string with an `fk` is otherwise data: for example, `utl-hpc`'s `partitions` rows begin with a model name, an `fk` to `sim-nam.models.mname`, but are an untagged table. + #### Integer Type `integer`. @@ -462,7 +467,7 @@ Type `integer`. ###### `fk` -`string | null (default: null)`. Marks this scalar as a foreign key. Valid only on integer or string scalars that are columns in a list item record. Two forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) bare block name (e.g., `"packagedata"`) — used together with `fk_ref` to name the block within the runtime-resolved target component, leaving only the pk field to be discovered. See "Primary/foreign keys". +`string | null (default: null)`. Marks this scalar as a foreign key. Valid only on integer or string scalars that are columns in a list item record. Two forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) bare block name (e.g., `"packagedata"`) — naming a block whose `pk` field is the target: in this component when used alone, or, together with `fk_ref`, within the runtime-resolved target component. See "Primary/foreign keys". ###### `fk_ref` @@ -560,7 +565,7 @@ Dimensions are listed fastest-varying first, i.e. in the order elements are read ###### `fk` -`string | null (default: null)`. Marks the array's (nonzero) elements as a foreign key, each a reference to a row (by `pk`) in another list. On a grid array that's a per-grid-cell reference, rather than the per-list-row reference a scalar `fk` expresses (e.g. a grid-wide array giving each cell's cross-section id, referencing the cross-section package's `packagedata`). On an array column in a list item record, each row references several rows (e.g. SFR's `ic`, the reaches connected to a reach). Only valid when `dtype` is `"integer"`. Hierarchical path form only (`"[component.]block.field"`) — an array has no `fk_ref` counterpart, since it has no sibling record to carry a runtime component-selector field, and no `pk` counterpart, since it has no rows of its own to be a key of. See "Primary/foreign keys". +`string | null (default: null)`. Marks the array's (nonzero) elements as a foreign key, each a reference to a row (by `pk`) in another list. On a grid array that's a per-grid-cell reference, rather than the per-list-row reference a scalar `fk` expresses (e.g. a grid-wide array giving each cell's cross-section id, referencing the cross-section package's `packagedata`). On an array column in a list item record, each row references several rows (e.g. SFR's `ic`, the reaches connected to a reach). On a `"string"` array, each entry is a key: a row's `pk` value, or, as for a string scalar, an entry of a string array the path names. Only valid when `dtype` is `"integer"` or `"string"`. Hierarchical path form only (`"[component.]block.field"`) — an array has no `fk_ref` counterpart, since it has no sibling record to carry a runtime component-selector field, and no `pk` counterpart, since it has no rows of its own to be a key of. See "Primary/foreign keys". ###### `model`, `allow_zero` @@ -598,7 +603,7 @@ A reader tries an untagged union's arms in order. A `keyword` arm is its own nam Type `list`. Collection type. Unlimited but for one rule: a list may not contain another list. Lists are distinct from arrays in two ways: a list element may be a composite type and a list admits sparse representations. -A list is **tagged** if and only if its `item` type is keyword-led, i.e. tagged. This is a property of the definition, not of any particular input file. A type is keyword-led if it is a `keyword`, a tagged scalar or array (which begins with its own name), or a record whose first field is keyword-led or is a **dynamic keyword**: an untagged `string` whose `fk` is a hierarchical path to a string array, so that its value is one of that array's entries. A dynamic keyword's possible values are known keywords once the array has been read, though not when the definition is written. A `union` item type is keyword-led if every arm is, since each arm is a distinct line form the item may take. For example, a record item `TS6 FILEIN `, or a union item whose arms are `ALL` and `FREQUENCY `. Otherwise it is **untagged**. This is derived from `item` rather than declared (like a block's optionality), and determines where the list may appear in its block (see [Field ordering](#field-ordering)). +A list is **tagged** if and only if its `item` type is keyword-led, i.e. tagged. This is a property of the definition, not of any particular input file. A type is keyword-led if it is a `keyword`, a tagged scalar or array (which begins with its own name), or a record whose first field is keyword-led or is a **dynamic keyword**: a `string` marked [`dynamic`](#dynamic), whose value is one of the keys its `fk` names, e.g. an entry of a string array. A dynamic keyword's possible values are known keywords once its `fk` target has been read, though not when the definition is written. A `union` item type is keyword-led if every arm is, since each arm is a distinct line form the item may take. For example, a record item `TS6 FILEIN `, or a union item whose arms are `ALL` and `FREQUENCY `. Otherwise it is **untagged**. This is derived from `item` rather than declared (like a block's optionality), and determines where the list may appear in its block (see [Field ordering](#field-ordering)). Untagged lists are tables: a block body of items, one per line, e.g. a stress package's period data. Tagged lists also express a line that may be repeated among a block's other fields, e.g. `TS6 FILEIN `, which MODFLOW 6 accepts any number of times in a package's options block (one per time-series file). A tagged list whose item begins with a dynamic keyword expresses a field whose name is chosen by the user. For example, an array-based stress package (e.g. `gwf-rcha`) reads one grid array per auxiliary variable, each introduced by that variable's name: @@ -614,6 +619,7 @@ aux: type: string tagged: false fk: options.auxiliary + dynamic: true aux: type: array dtype: double @@ -840,12 +846,12 @@ Only a dimension that every component in the model (or simulation) shares should 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`, `fk`, and `fk_ref`. 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`/`fk_ref` are 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-`dtype` `Array` fields (see [Array](#array)) — there it references another list's row per grid cell rather than per list row, and only the hierarchical-path form applies (no `fk_ref` counterpart, since an array has no sibling record to carry a runtime component selector). +**Note**: `pk`/`fk_ref` are 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. Only the hierarchical-path form applies (no `fk_ref` counterpart, since an array has no sibling record to carry a runtime component selector). The `fk` attribute can take one of two forms: -- **Hierarchical path**: A path of the form `"[component.]block.field"`, for use when the primary key field is statically known. The `component` segment is necessary for cross-component references; it may be omitted for within-component references. The hierarchical path form may not be used with `fk_ref`. On a `string` field, the path may instead name a string array (e.g. `"options.auxiliary"`), whose entries then act as the key set; no `pk` is involved. A dynamic keyword (see [List](#list)) must use this form. -- **Bare block name**: The name of a block in which there is exactly one `pk` field. In this case, `fk_ref` is required to resolve the target component at runtime. +- **Hierarchical path**: A path of the form `"[component.]block.field"`, for use when the primary key field is statically known. The `component` segment is necessary for cross-component references; it may be omitted for within-component references. The hierarchical path form may not be used with `fk_ref`. 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. +- **Bare block name**: The name of a block in which there is exactly one `pk` field. Alone, the block is looked up in this component, so the reference is as static as a hierarchical path, which may be preferred for clarity. With `fk_ref`, the block is looked up in the component `fk_ref` resolves to at runtime (see below). The `fk_ref` attribute names a string field whose runtime value identifies the component containing the `pk` field. Two sub-cases exist: 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 19bd380c..6e306b3c 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1579,6 +1579,90 @@ def _fix_mvr_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B return {**blocks, "packages": new_packages, "period": new_period} +def _list_item_field(blocks: dict[str, v2.Block], block_name: str, field_name: str): + """The field `field_name` of the item record of the list sharing + `block_name`'s name, or None if there's no such field.""" + block = blocks.get(block_name) + lst = block.fields.get(block_name) if block else None + item = lst.item if isinstance(lst, v2.List) else None + return item.fields.get(field_name) if isinstance(item, v2.Record) else None + + +def _set_list_item_attrs( + blocks: dict[str, v2.Block], block_name: str, attrs: dict[str, dict[str, Any]] +) -> dict[str, v2.Block]: + """ + Set attributes on fields of the item record of the list sharing + `block_name`'s name, e.g. `{"mname": {"pk": True}}`, leaving any that's + already set. The fields must exist (see `_list_item_field`). + """ + block = blocks[block_name] + lst = block.fields[block_name] + item_fields = dict(lst.item.fields) # type: ignore[union-attr] + for field_name, field_attrs in attrs.items(): + field = item_fields[field_name] + update = {k: v for k, v in field_attrs.items() if not getattr(field, k)} + if update: + item_fields[field_name] = field.model_copy(update=update) + new_item = lst.item.model_copy(update={"fields": item_fields}) # type: ignore[union-attr] + new_list = lst.model_copy(update={"item": new_item}) + new_block = block.model_copy(update={"fields": {**block.fields, block_name: new_list}}) + return {**blocks, block_name: new_block} + + +def _fix_model_name_relations(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + """ + Patch the model-name pk/fk relations of `sim-nam` and `utl-hpc`, which + the general passes can't reach on their own. + + - Marks `models.mname` as `pk`. MF6 rejects a duplicate model name + (`check_model_name` in `SimulationCreate.f90`), comparing names + upcased, i.e. case-insensitively like a `String` by default. It isn't + the leading column (`mtype` is), and nothing else repeats the name + `mname`, so neither `_resolve_relations` nor `_mark_lonely_pk` finds it. + - Marks `exchanges`' `exgmnamea`/`exgmnameb` and `solutiongroup`'s + `slnmnames` (a string array, each entry a model name) as + `fk: "models.mname"`, since MF6 looks each up among the model names + and fails on one not found. + - Marks `utl-hpc`'s `partitions.mname` as `fk: "sim-nam.models.mname"`: + MF6 rejects an undefined model name (`DistributedSim.f90`). Not a `pk`, + since MF6 doesn't reject a repeat (the last rank given wins). It leads + its row, but as data, not a dynamic keyword, so `partitions` stays + untagged. + + Doesn't express which model *types* are valid where (a GWF-GWT + exchange's model A must be a GWF model; IMS can't solve an explicit + model): those are constraints on the referenced row, not keys. + """ + if name == "utl-hpc": + if not isinstance(_list_item_field(blocks, "partitions", "mname"), v2.String): + return blocks + return _set_list_item_attrs(blocks, "partitions", {"mname": {"fk": "sim-nam.models.mname"}}) + if name != "sim-nam": + return blocks + + refs = { + "models": ("mname",), + "exchanges": ("exgmnamea", "exgmnameb"), + "solutiongroup": ("slnmnames",), + } + for block_name, field_names in refs.items(): + for field_name in field_names: + field = _list_item_field(blocks, block_name, field_name) + if not ( + isinstance(field, v2.String) + or (isinstance(field, v2.Array) and field.dtype == "string") + ): + return blocks + + blocks = _set_list_item_attrs(blocks, "models", {"mname": {"pk": True}}) + for block_name in ("exchanges", "solutiongroup"): + blocks = _set_list_item_attrs( + blocks, block_name, {n: {"fk": "models.mname"} for n in refs[block_name]} + ) + return blocks + + def _fix_ssm_sources_write_if_empty(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: """ Mark SSM's SOURCES block write_if_empty: MF6 requires the block's header @@ -1705,8 +1789,8 @@ def _wrap_grid_aux(blocks: dict[str, v2.Block], fields: OMD) -> dict[str, v2.Blo ``AUX``, but the input file has zero or more grid arrays, each introduced by one of the ``auxiliary`` names (MF6 treats any period tag matching one as ``AUX``). The leading ``auxname`` is a dynamic keyword (an untagged - String with an ``fk`` to ``options.auxiliary``), which makes the list - tagged, so it mixes freely with the block's other arrays. Each auxiliary + String marked ``dynamic``, with an ``fk`` to ``options.auxiliary``), which + makes the list tagged, so it mixes freely with the block's other arrays. Each auxiliary variable is set at most once per period, so the list is bounded by the ``auxiliary`` count, and is always optional (v1's ``gwf-evta`` says otherwise). The outer name stays ``aux`` so memory variables sourced from @@ -1738,6 +1822,7 @@ def _wrap_grid_aux(blocks: dict[str, v2.Block], fields: OMD) -> dict[str, v2.Blo "one of the auxiliary variables.", tagged=False, fk="options.auxiliary", + dynamic=True, ) array = field.model_copy(update={"tagged": False, "optional": False, "description": None}) updates[fname] = v2.List( @@ -2326,6 +2411,7 @@ def _lookup(rname: str) -> dict | None: blocks = _patch_oc_rtype(name, blocks) blocks = _fix_lak_relations(name, blocks) blocks = _fix_mvr_relations(name, blocks) + blocks = _fix_model_name_relations(name, blocks) blocks = _fix_ssm_sources_write_if_empty(name, blocks) blocks = _fix_optional_blocks(name, blocks) blocks = _wrap_grid_aux(blocks, fields) diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 154fe276..3efbc2ff 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -147,6 +147,27 @@ class String(InputFieldBase): pk: bool = False fk: str | None = None fk_ref: str | None = None + # A dynamic keyword: the value itself is a keyword, one of the `fk` + # target's keys, so a record it leads is keyword-led (see + # `_is_dynamic_key`). A role in the input grammar, independent of `fk`, + # which only says what the value references. + dynamic: bool = False + + @model_validator(mode="after") + def _check_dynamic(self) -> "String": + if not self.dynamic: + return self + if self.tagged: + raise ValueError( + f"String {self.name!r}: dynamic requires tagged=False (its value is the keyword)" + ) + # Its keywords must be known before its line is read, so they can't + # depend on a sibling later on that line, as `fk_ref` would make them. + if self.fk is None or self.fk_ref is not None: + raise ValueError( + f"String {self.name!r}: dynamic requires an fk naming its keywords, without fk_ref" + ) + return self # `True`: a 1-based index in the file, 0-based in memory. `"signed"`: the @@ -217,6 +238,9 @@ class Array(InputFieldBase): time_series: bool = False layered: bool = False index: Index = False + # Integer: each (nonzero) element references a row by `pk`, per grid cell + # or, in a list item, per entry. String: each entry is a key into the + # target (e.g. a list of model names). Hierarchical-path form only. fk: str | None = None # For dtype="cellid" only: each element is a `CellID` with these. model: CellModel = None @@ -241,10 +265,18 @@ def _check_index_dtype(self) -> "Array": raise ValueError( f"Array {self.name!r}: {attr} is not valid with dtype='cellid'" ) - if self.fk is not None and self.dtype != "integer": - raise ValueError( - f"Array {self.name!r}: fk requires dtype='integer', got {self.dtype!r}" - ) + if self.fk is not None: + if self.dtype not in ("integer", "string"): + raise ValueError( + f"Array {self.name!r}: fk requires dtype='integer' or 'string', " + f"got {self.dtype!r}" + ) + # no sibling `fk_ref` to resolve a bare block name against + if "." not in self.fk: + raise ValueError( + f"Array {self.name!r}: fk={self.fk!r} must be a hierarchical path " + f"('[component.]block.field')" + ) return self @@ -256,6 +288,16 @@ def _derive_tagged(self) -> bool: # Its first field begins with a keyword, or is a dynamic keyword. return _item_tags(self) is not None + @model_validator(mode="after") + def _check_dynamic_leads(self) -> "Record": + # A dynamic keyword begins a line; anywhere else it'd be plain data. + later = [n for n, f in list(self.fields.items())[1:] if _is_dynamic_key(f)] + if later: + raise ValueError( + f"Record {self.name!r}: only the first field may be dynamic, got {later!r}" + ) + return self + @property def children(self) -> "dict[str, InputField]": return self.fields # type: ignore[return-value] @@ -275,19 +317,14 @@ def children(self) -> "dict[str, InputField]": def _is_dynamic_key(field: "InputField | None") -> bool: - """Whether `field` is a dynamic keyword: an untagged String whose value is - one of the entries of a string array named by a hierarchical-path `fk` - (e.g. an auxiliary variable's name). Its possible values are known keywords - once that array has been read, so a record it leads is keyword-led. That - the `fk` target really is a string array is checked by `_validate_fk_fields`, - since it needs the enclosing component.""" - return ( - isinstance(field, String) - and not field.tagged - and field.fk is not None - and "." in field.fk - and field.fk_ref is None - ) + """Whether `field` is a dynamic keyword: a String marked `dynamic`, whose + value is one of the keys its `fk` names (e.g. an auxiliary variable's + name, one of `options.auxiliary`'s entries). Its possible values are known + keywords once the target has been read, so a record it leads is + keyword-led. Declared rather than inferred from `fk`, since a leading + untagged String with an `fk` may just as well be data (e.g. `utl-hpc`'s + `partitions.mname`, a model name).""" + return isinstance(field, String) and field.dynamic def _leads_with_dynamic_key(form: "InputField") -> bool: @@ -1460,22 +1497,22 @@ def _validate_list_shape_element( def _validate_fk_fields(component: "ComponentBase", spec: "Dfns") -> None: """ For every Integer/String/Array field with fk or fk_ref set, validate - structure. Array's `fk` (per-grid-cell rather than per-list-row) only - ever uses the hierarchical-path form below — it has no `pk`/`fk_ref` - counterpart (see `Array.fk`). + structure. Array's `fk` (an integer array's per grid cell or per entry, + a string array's per entry) only ever uses the hierarchical-path form + below — it has no `pk`/`fk_ref` counterpart (see `Array.fk`). Two forms (see docs/md/dfnspec.md, "Primary and foreign keys"); grid-cell references are a separate mechanism entirely (the `cellid` type, not an `fk` value — see `CellID`): - Hierarchical path fk ("[component.]block.field", no fk_ref): the named - block must be a list block whose item has a pk field, or, for a String, - the named field may instead be a string-dtype Array, whose entries are - then the key set (e.g. `options.auxiliary`). Unqualified ("block.field"), - the block is looked up in this component; qualified - ("component.block.field"), it's looked up in the named component via - `spec.components` instead. A dynamic key (see `_is_dynamic_key`) must - take the string-array form. + block must be a list block whose item has a pk field, or, for a String + or string Array, the named field may instead be a string-dtype Array, + whose entries are then the key set (e.g. `options.auxiliary`). + Unqualified ("block.field"), the block is looked up in this component; + qualified ("component.block.field"), it's looked up in the named + component via `spec.components` instead. A dynamic key (see + `_is_dynamic_key`) is checked like any other String. - Bare block name fk + fk_ref, or fk_ref alone: fk_ref must name a sibling String field in the same record, whose runtime value identifies the target component (and, with fk, the pk field is looked up in the block @@ -1487,7 +1524,7 @@ def _validate_fk_fields(component: "ComponentBase", spec: "Dfns") -> None: Observation fields (see `ObservationField`) are checked like a list item's. """ - def _check_fields(fields: dict, dynamic_key: "InputField | None" = None) -> None: + def _check_fields(fields: dict) -> None: for field in fields.values(): fk: str | None = getattr(field, "fk", None) fk_ref: str | None = getattr(field, "fk_ref", None) @@ -1530,18 +1567,14 @@ def _check_fields(fields: dict, dynamic_key: "InputField | None" = None) -> None f"must be a bare block name, 'block.field', or " f"'component.block.field'" ) - is_dynamic_key = field is dynamic_key - if is_dynamic_key or (isinstance(field, String) and len(parts) > 1): + is_string = isinstance(field, String) or ( + isinstance(field, Array) and field.dtype == "string" + ) + if is_string and len(parts) > 1: target_block = (target.blocks or {}).get(block_name) target_field = target_block.fields.get(parts[-1]) if target_block else None if isinstance(target_field, Array) and target_field.dtype == "string": continue - if is_dynamic_key: - raise ValueError( - f"Field {field.name!r} fk={fk!r}: leads a tagged list's " - f"item as a dynamic keyword, so must name a string array " - f"in {where}" - ) list_field = _find_list_in_block(target, block_name) if list_field is None: raise ValueError( @@ -1567,8 +1600,7 @@ def _check_fields(fields: dict, dynamic_key: "InputField | None" = None) -> None # a Record item of its own (a scalar arm has no siblings). for form in [item] if isinstance(item, Record) else item.arms.values(): if isinstance(form, Record): - lead = next(iter(form.fields.values()), None) - _check_fields(form.fields, lead if _is_dynamic_key(lead) else None) + _check_fields(form.fields) else: _check_fields({form.name: form})