From dbdc314417c0b2bf07322423d153c13e85d2717f Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 08:00:03 -0700 Subject: [PATCH] fix(dfns): type number-or-word fields as unions v1 types a few fields `string` when MF6 reads a number from them. LAK's `bedleak` takes a number or NONE, and MAW's `head_limit` a number or OFF. The migration now makes each an untagged union, a keyword arm tried before a double arm. A union can't be a union's arm, so MAW's `head_limit` arm becomes a record, `HEAD_LIMIT `. It also retypes UZF's seven `string` time-series period values as `double`. mf6 made the same change on develop after 6.8.1 (#2786), so the migration also accepts DFNs that already have it. Both fixup tables raise if a named field is missing or has an unexpected type, so they can't silently go stale. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/v2.0.0.dev2/gwf-lak.json | 13 +- .../__snapshots__/v2.0.0.dev2/gwf-lak.toml | 9 +- .../__snapshots__/v2.0.0.dev2/gwf-lak.yaml | 8 +- .../__snapshots__/v2.0.0.dev2/gwf-maw.json | 26 +++- .../__snapshots__/v2.0.0.dev2/gwf-maw.toml | 18 ++- .../__snapshots__/v2.0.0.dev2/gwf-maw.yaml | 34 +++-- .../__snapshots__/v2.0.0.dev2/gwf-uzf.json | 14 +-- .../__snapshots__/v2.0.0.dev2/gwf-uzf.toml | 14 +-- .../__snapshots__/v2.0.0.dev2/gwf-uzf.yaml | 14 +-- .../__snapshots__/v2.0.0.dev3/gwf-lak.json | 13 +- .../__snapshots__/v2.0.0.dev3/gwf-lak.toml | 9 +- .../__snapshots__/v2.0.0.dev3/gwf-lak.yaml | 8 +- .../__snapshots__/v2.0.0.dev3/gwf-maw.json | 26 +++- .../__snapshots__/v2.0.0.dev3/gwf-maw.toml | 18 ++- .../__snapshots__/v2.0.0.dev3/gwf-maw.yaml | 34 +++-- .../__snapshots__/v2.0.0.dev3/gwf-uzf.json | 14 +-- .../__snapshots__/v2.0.0.dev3/gwf-uzf.toml | 14 +-- .../__snapshots__/v2.0.0.dev3/gwf-uzf.yaml | 14 +-- autotest/dfns/test_migrate.py | 64 ++++++++++ docs/md/dfnspec.md | 2 + .../dfns/migrate_to_v2_0_0_dev2.py | 118 ++++++++++++++++++ 21 files changed, 400 insertions(+), 84 deletions(-) diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.json index ab4fe01d..02298f03 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.json @@ -329,10 +329,19 @@ "tagged": false }, "bedleak": { - "type": "string", + "type": "union", "longname": "bed leakance", "description": "real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value (3.0E+30), or specified to be NONE. If DNODATA or NONE is specified for BEDLEAK, the lake-GWF connection conductance is solely a function of aquifer properties in the connected GWF cell and lakebed sediments are assumed to be absent. Warning messages will be issued if NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error.", - "tagged": false + "tagged": false, + "arms": { + "none": { + "type": "keyword" + }, + "value": { + "type": "double", + "tagged": false + } + } }, "belev": { "type": "double", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.toml index 697b0e68..d4d99003 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.toml @@ -302,11 +302,18 @@ description = "character string that defines the lake-GWF connection type for th tagged = false [blocks.connectiondata.fields.connectiondata.item.fields.bedleak] -type = "string" +type = "union" longname = "bed leakance" description = "real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value (3.0E+30), or specified to be NONE. If DNODATA or NONE is specified for BEDLEAK, the lake-GWF connection conductance is solely a function of aquifer properties in the connected GWF cell and lakebed sediments are assumed to be absent. Warning messages will be issued if NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error." tagged = false +[blocks.connectiondata.fields.connectiondata.item.fields.bedleak.arms.none] +type = "keyword" + +[blocks.connectiondata.fields.connectiondata.item.fields.bedleak.arms.value] +type = "double" +tagged = false + [blocks.connectiondata.fields.connectiondata.item.fields.belev] type = "double" longname = "bottom elevation" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.yaml index eee7b54e..5a4f54e9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-lak.yaml @@ -340,7 +340,7 @@ blocks: cell (NLAKECONN = 1) and there must be a lake table associated with each embedded lake.' tagged: false bedleak: - type: string + type: union longname: bed leakance description: real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value @@ -350,6 +350,12 @@ blocks: NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error. tagged: false + arms: + none: + type: keyword + value: + type: double + tagged: false belev: type: double longname: bottom elevation diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.json index 203e542a..9d417800 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.json @@ -415,10 +415,28 @@ "description": "is the head in the multi-aquifer well. WELL_HEAD is only applied to constant head (STATUS is CONSTANT) and inactive (STATUS is INACTIVE) multi-aquifer wells. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well.", "time_series": true }, - "head_limit": { - "type": "string", - "longname": "head limit", - "description": "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'." + "head_limitrecord": { + "type": "record", + "fields": { + "head_limit": { + "type": "keyword" + }, + "head_limit_value": { + "type": "union", + "longname": "head limit", + "description": "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'.", + "tagged": false, + "arms": { + "off": { + "type": "keyword" + }, + "value": { + "type": "double", + "tagged": false + } + } + } + } }, "shutoffrecord": { "type": "record", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.toml index 2206e8cd..aa45bf33 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.toml @@ -377,10 +377,24 @@ longname = "well head" description = "is the head in the multi-aquifer well. WELL_HEAD is only applied to constant head (STATUS is CONSTANT) and inactive (STATUS is INACTIVE) multi-aquifer wells. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well." time_series = true -[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limit] -type = "string" +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord] +type = "record" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit] +type = "keyword" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value] +type = "union" longname = "head limit" description = "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'." +tagged = false + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value.arms.off] +type = "keyword" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value.arms.value] +type = "double" +tagged = false [blocks.period.fields.perioddata.item.fields.mawsetting.arms.shutoffrecord] type = "record" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.yaml index 42014678..df24b4ee 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-maw.yaml @@ -472,17 +472,29 @@ blocks: of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well. time_series: true - head_limit: - type: string - longname: head limit - description: is the limiting water level (head) in the well, which is the minimum of - the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied - to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be - deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on - the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for - MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility - with previous versions of MODFLOW but use of the RATE_SCALING option instead of the - HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'. + head_limitrecord: + type: record + fields: + head_limit: + type: keyword + head_limit_value: + type: union + longname: head limit + description: is the limiting water level (head) in the well, which is the minimum + of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied + to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can + be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based + on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package + for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward + compatibility with previous versions of MODFLOW but use of the RATE_SCALING option + instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'. + tagged: false + arms: + 'off': + type: keyword + value: + type: double + tagged: false shutoffrecord: type: record fields: diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.json index 58fe595e..5c26f26d 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.json @@ -347,49 +347,49 @@ "fk": "packagedata.ifno" }, "finf": { - "type": "string", + "type": "double", "longname": "infiltration rate", "description": "real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "pet": { - "type": "string", + "type": "double", "longname": "potential ET rate", "description": "real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated zone and any remaining potential evapotranspiration is applied to the saturated zone. If IVERTCON is greater than zero then residual potential evapotranspiration not satisfied in the UZF cell is applied to the underlying UZF and GWF cells. PET is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "extdp": { - "type": "string", + "type": "double", "longname": "extinction depth", "description": "real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell bottom then remaining potential evapotranspiration is applied to the underlying UZF and GWF cells. EXTDP is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "extwc": { - "type": "string", + "type": "double", "longname": "extinction water content", "description": "real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and UNSAT_ETWC are specified in the OPTIONS block. The evapotranspiration rate from the unsaturated zone will be set to zero when the calculated water content is at or less than this value. The value for EXTWC cannot be less than the residual water content, and if it is specified as being less than the residual water content it is set to the residual water content. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "ha": { - "type": "string", + "type": "double", "longname": "air entry potential", "description": "real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "hroot": { - "type": "string", + "type": "double", "longname": "root potential", "description": "real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "rootact": { - "type": "string", + "type": "double", "longname": "root activity function", "description": "real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. Values range from 0 to about 3 $cm^{-2}$, depending on the plant community and its stage of development. ROOTACT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.toml index ce2d9003..32cd8d89 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.toml @@ -317,49 +317,49 @@ index = true fk = "packagedata.ifno" [blocks.period.fields.perioddata.item.fields.finf] -type = "string" +type = "double" longname = "infiltration rate" description = "real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.pet] -type = "string" +type = "double" longname = "potential ET rate" description = "real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated zone and any remaining potential evapotranspiration is applied to the saturated zone. If IVERTCON is greater than zero then residual potential evapotranspiration not satisfied in the UZF cell is applied to the underlying UZF and GWF cells. PET is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.extdp] -type = "string" +type = "double" longname = "extinction depth" description = "real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell bottom then remaining potential evapotranspiration is applied to the underlying UZF and GWF cells. EXTDP is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.extwc] -type = "string" +type = "double" longname = "extinction water content" description = "real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and UNSAT_ETWC are specified in the OPTIONS block. The evapotranspiration rate from the unsaturated zone will be set to zero when the calculated water content is at or less than this value. The value for EXTWC cannot be less than the residual water content, and if it is specified as being less than the residual water content it is set to the residual water content. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.ha] -type = "string" +type = "double" longname = "air entry potential" description = "real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.hroot] -type = "string" +type = "double" longname = "root potential" description = "real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.rootact] -type = "string" +type = "double" longname = "root activity function" description = "real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. Values range from 0 to about 3 $cm^{-2}$, depending on the plant community and its stage of development. ROOTACT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.yaml index 55d84149..64894741 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/gwf-uzf.yaml @@ -343,7 +343,7 @@ blocks: index: true fk: packagedata.ifno finf: - type: string + type: double longname: infiltration rate description: real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable @@ -352,7 +352,7 @@ blocks: tagged: false time_series: true pet: - type: string + type: double longname: potential ET rate description: real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated @@ -365,7 +365,7 @@ blocks: tagged: false time_series: true extdp: - type: string + type: double longname: extinction depth description: real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell @@ -377,7 +377,7 @@ blocks: tagged: false time_series: true extwc: - type: string + type: double longname: extinction water content description: real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and @@ -391,7 +391,7 @@ blocks: tagged: false time_series: true ha: - type: string + type: double longname: air entry potential description: real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified @@ -401,7 +401,7 @@ blocks: tagged: false time_series: true hroot: - type: string + type: double longname: root potential description: real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified @@ -411,7 +411,7 @@ blocks: tagged: false time_series: true rootact: - type: string + type: double longname: root activity function description: real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.json index f4d7616b..5cc9f149 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.json @@ -329,10 +329,19 @@ "tagged": false }, "bedleak": { - "type": "string", + "type": "union", "longname": "bed leakance", "description": "real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value (3.0E+30), or specified to be NONE. If DNODATA or NONE is specified for BEDLEAK, the lake-GWF connection conductance is solely a function of aquifer properties in the connected GWF cell and lakebed sediments are assumed to be absent. Warning messages will be issued if NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error.", - "tagged": false + "tagged": false, + "arms": { + "none": { + "type": "keyword" + }, + "value": { + "type": "double", + "tagged": false + } + } }, "belev": { "type": "double", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.toml index 67ae5730..023621a9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.toml @@ -302,11 +302,18 @@ description = "character string that defines the lake-GWF connection type for th tagged = false [blocks.connectiondata.fields.connectiondata.item.fields.bedleak] -type = "string" +type = "union" longname = "bed leakance" description = "real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value (3.0E+30), or specified to be NONE. If DNODATA or NONE is specified for BEDLEAK, the lake-GWF connection conductance is solely a function of aquifer properties in the connected GWF cell and lakebed sediments are assumed to be absent. Warning messages will be issued if NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error." tagged = false +[blocks.connectiondata.fields.connectiondata.item.fields.bedleak.arms.none] +type = "keyword" + +[blocks.connectiondata.fields.connectiondata.item.fields.bedleak.arms.value] +type = "double" +tagged = false + [blocks.connectiondata.fields.connectiondata.item.fields.belev] type = "double" longname = "bottom elevation" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.yaml index 34d44346..681d98a9 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-lak.yaml @@ -340,7 +340,7 @@ blocks: cell (NLAKECONN = 1) and there must be a lake table associated with each embedded lake.' tagged: false bedleak: - type: string + type: union longname: bed leakance description: real value or character string that defines the bed leakance for the lake-GWF connection. BEDLEAK must be greater than or equal to zero, equal to the DNODATA value @@ -350,6 +350,12 @@ blocks: NONE is specified. Eventually the ability to specify NONE will be deprecated and cause MODFLOW 6 to terminate with an error. tagged: false + arms: + none: + type: keyword + value: + type: double + tagged: false belev: type: double longname: bottom elevation diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.json index a67446b4..c2eda39c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.json @@ -415,10 +415,28 @@ "description": "is the head in the multi-aquifer well. WELL_HEAD is only applied to constant head (STATUS is CONSTANT) and inactive (STATUS is INACTIVE) multi-aquifer wells. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well.", "time_series": true }, - "head_limit": { - "type": "string", - "longname": "head limit", - "description": "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'." + "head_limitrecord": { + "type": "record", + "fields": { + "head_limit": { + "type": "keyword" + }, + "head_limit_value": { + "type": "union", + "longname": "head limit", + "description": "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'.", + "tagged": false, + "arms": { + "off": { + "type": "keyword" + }, + "value": { + "type": "double", + "tagged": false + } + } + } + } }, "shutoffrecord": { "type": "record", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.toml index b07b7eab..9547458c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.toml @@ -377,10 +377,24 @@ longname = "well head" description = "is the head in the multi-aquifer well. WELL_HEAD is only applied to constant head (STATUS is CONSTANT) and inactive (STATUS is INACTIVE) multi-aquifer wells. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well." time_series = true -[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limit] -type = "string" +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord] +type = "record" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit] +type = "keyword" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value] +type = "union" longname = "head limit" description = "is the limiting water level (head) in the well, which is the minimum of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE_SCALING option instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'." +tagged = false + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value.arms.off] +type = "keyword" + +[blocks.period.fields.perioddata.item.fields.mawsetting.arms.head_limitrecord.fields.head_limit_value.arms.value] +type = "double" +tagged = false [blocks.period.fields.perioddata.item.fields.mawsetting.arms.shutoffrecord] type = "record" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.yaml index abf00972..a781c393 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-maw.yaml @@ -472,17 +472,29 @@ blocks: of a numeric value. The program will terminate with an error if WELL_HEAD is less than the bottom of the well. time_series: true - head_limit: - type: string - longname: head limit - description: is the limiting water level (head) in the well, which is the minimum of - the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied - to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can be - deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based on - the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package for - MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward compatibility - with previous versions of MODFLOW but use of the RATE_SCALING option instead of the - HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'. + head_limitrecord: + type: record + fields: + head_limit: + type: keyword + head_limit_value: + type: union + longname: head limit + description: is the limiting water level (head) in the well, which is the minimum + of the well RATE or the well inflow rate from the aquifer. HEAD_LIMIT can be applied + to extraction wells (RATE $<$ 0) or injection wells (RATE $>$ 0). HEAD_LIMIT can + be deactivated by specifying the text string `OFF'. The HEAD_LIMIT option is based + on the HEAD_LIMIT functionality available in the MNW2~citep{konikow2009} package + for MODFLOW-2005. The HEAD_LIMIT option has been included to facilitate backward + compatibility with previous versions of MODFLOW but use of the RATE_SCALING option + instead of the HEAD_LIMIT option is recommended. By default, HEAD_LIMIT is `OFF'. + tagged: false + arms: + 'off': + type: keyword + value: + type: double + tagged: false shutoffrecord: type: record fields: diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.json index 08f5299a..32c0b5fd 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.json @@ -347,49 +347,49 @@ "fk": "packagedata.ifno" }, "finf": { - "type": "string", + "type": "double", "longname": "infiltration rate", "description": "real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "pet": { - "type": "string", + "type": "double", "longname": "potential ET rate", "description": "real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated zone and any remaining potential evapotranspiration is applied to the saturated zone. If IVERTCON is greater than zero then residual potential evapotranspiration not satisfied in the UZF cell is applied to the underlying UZF and GWF cells. PET is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "extdp": { - "type": "string", + "type": "double", "longname": "extinction depth", "description": "real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell bottom then remaining potential evapotranspiration is applied to the underlying UZF and GWF cells. EXTDP is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "extwc": { - "type": "string", + "type": "double", "longname": "extinction water content", "description": "real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and UNSAT_ETWC are specified in the OPTIONS block. The evapotranspiration rate from the unsaturated zone will be set to zero when the calculated water content is at or less than this value. The value for EXTWC cannot be less than the residual water content, and if it is specified as being less than the residual water content it is set to the residual water content. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "ha": { - "type": "string", + "type": "double", "longname": "air entry potential", "description": "real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "hroot": { - "type": "string", + "type": "double", "longname": "root potential", "description": "real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, "time_series": true }, "rootact": { - "type": "string", + "type": "double", "longname": "root activity function", "description": "real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. Values range from 0 to about 3 $cm^{-2}$, depending on the plant community and its stage of development. ROOTACT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value.", "tagged": false, diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.toml index 5634e242..02c6ec26 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.toml @@ -317,49 +317,49 @@ index = true fk = "packagedata.ifno" [blocks.period.fields.perioddata.item.fields.finf] -type = "string" +type = "double" longname = "infiltration rate" description = "real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.pet] -type = "string" +type = "double" longname = "potential ET rate" description = "real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated zone and any remaining potential evapotranspiration is applied to the saturated zone. If IVERTCON is greater than zero then residual potential evapotranspiration not satisfied in the UZF cell is applied to the underlying UZF and GWF cells. PET is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.extdp] -type = "string" +type = "double" longname = "extinction depth" description = "real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell bottom then remaining potential evapotranspiration is applied to the underlying UZF and GWF cells. EXTDP is always specified, but is only used if SIMULATE_ET is specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.extwc] -type = "string" +type = "double" longname = "extinction water content" description = "real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and UNSAT_ETWC are specified in the OPTIONS block. The evapotranspiration rate from the unsaturated zone will be set to zero when the calculated water content is at or less than this value. The value for EXTWC cannot be less than the residual water content, and if it is specified as being less than the residual water content it is set to the residual water content. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.ha] -type = "string" +type = "double" longname = "air entry potential" description = "real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.hroot] -type = "string" +type = "double" longname = "root potential" description = "real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false time_series = true [blocks.period.fields.perioddata.item.fields.rootact] -type = "string" +type = "double" longname = "root activity function" description = "real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. Values range from 0 to about 3 $cm^{-2}$, depending on the plant community and its stage of development. ROOTACT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value." tagged = false diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.yaml index 7353070e..9206e9b7 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/gwf-uzf.yaml @@ -343,7 +343,7 @@ blocks: index: true fk: packagedata.ifno finf: - type: string + type: double longname: infiltration rate description: real or character value that defines the applied infiltration rate of the UZF cell ($LT^{-1}$). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable @@ -352,7 +352,7 @@ blocks: tagged: false time_series: true pet: - type: string + type: double longname: potential ET rate description: real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated @@ -365,7 +365,7 @@ blocks: tagged: false time_series: true extdp: - type: string + type: double longname: extinction depth description: real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell @@ -377,7 +377,7 @@ blocks: tagged: false time_series: true extwc: - type: string + type: double longname: extinction water content description: real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and @@ -391,7 +391,7 @@ blocks: tagged: false time_series: true ha: - type: string + type: double longname: air entry potential description: real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified @@ -401,7 +401,7 @@ blocks: tagged: false time_series: true hroot: - type: string + type: double longname: root potential description: real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified @@ -411,7 +411,7 @@ blocks: tagged: false time_series: true rootact: - type: string + type: double longname: root activity function description: real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 787f9036..27032eae 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -358,6 +358,70 @@ def test_migrate_exchange_cellids(dfn_dir, name): assert (field.shape, field.index, field.cellid) == (["ncelldim"], True, model) +def _number_or_word(field, word: str) -> None: + assert isinstance(field, v2.Union) + assert not field.tagged + assert list(field.arms) == [word, "value"] # the word is tried first + assert isinstance(field.arms[word], v2.Keyword) + assert isinstance(value := field.arms["value"], v2.Double) + assert not value.tagged + + +def test_migrate_lak_bedleak_number_or_none(dfn_dir): + component = _migrate_dev3(dfn_dir, "gwf-lak") + item = component.blocks["connectiondata"].fields["connectiondata"].item + assert list(item.fields)[4] == "bedleak" # still mid-row + bedleak = item.fields["bedleak"] + _number_or_word(bedleak, "none") + assert bedleak.longname == "bed leakance" + assert "NONE" in bedleak.description + + +def test_migrate_maw_head_limit_number_or_off(dfn_dir): + component = _migrate_dev3(dfn_dir, "gwf-maw") + item = component.blocks["period"].fields["perioddata"].item + arms = item.fields["mawsetting"].arms + assert "head_limit" not in arms + record = arms["head_limitrecord"] + assert isinstance(record, v2.Record) + assert list(record.fields) == ["head_limit", "head_limit_value"] + assert isinstance(record.fields["head_limit"], v2.Keyword) + value = record.fields["head_limit_value"] + _number_or_word(value, "off") + assert value.longname == "head limit" + assert record.render() == "HEAD_LIMIT " + + +def test_migrate_uzf_time_series_are_doubles(dfn_dir): + component = _migrate_dev3(dfn_dir, "gwf-uzf") + item = component.blocks["period"].fields["perioddata"].item + for name in ("finf", "pet", "extdp", "extwc", "ha", "hroot", "rootact"): + field = item.fields[name] + assert isinstance(field, v2.Double), name + assert field.time_series, name + + +def test_migrate_number_or_word_fails_when_stale(dfn_dir, tmp_path, monkeypatch): + from modflow_devtools.dfns import migrate_to_v2_0_0_dev2 as dev2 + + monkeypatch.setitem(dev2._NUMBER_OR_WORD, "gwf-lak", [("connectiondata", "nope", "none")]) + with pytest.raises(ValueError, match="nope"): + _migrate_dev3(dfn_dir, "gwf-lak") + monkeypatch.setitem(dev2._NUMBER_OR_WORD, "gwf-lak", [("connectiondata", "belev", "none")]) + with pytest.raises(ValueError, match="expected 'string'"): + _migrate_dev3(dfn_dir, "gwf-lak") + + +def test_migrate_number_or_word_round_trip(dev3): + out, _ = dev3 + components = v2.Dfns.load(out).components + lak = components["gwf-lak"].blocks["connectiondata"].fields["connectiondata"].item + _number_or_word(lak.fields["bedleak"], "none") + maw = components["gwf-maw"].blocks["period"].fields["perioddata"].item + arm = maw.fields["mawsetting"].arms["head_limitrecord"] + _number_or_word(arm.fields["head_limit_value"], "off") + + def test_migrate_round_trip_lossless(dfn_dir, dev3): # Writing the current schema and loading it back reproduces the # components loaded from the legacy DFNs exactly. diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 363bcf69..4d464968 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -543,6 +543,8 @@ Type `union`. Sum type. A union is usually keyword-led: each arm begins with its own keyword, which picks the arm. An untagged union's arms begin with a value instead, and the value alone may not pick the arm. `utl-obs`'s `id` is a cellid, a 1-based index (e.g. a lake number), or a boundname, depending on the parent package and the observation type. The parent's [`observations`](#observations) say which. +A reader tries an untagged union's arms in order. A `keyword` arm is its own name as a literal token, matched case-insensitively, so a keyword arm before a number arm expresses "a number, or this word". For example, `gwf-lak`'s `bedleak` has arms `none` (a keyword) and `value` (a double), and `gwf-maw`'s `HEAD_LIMIT` takes `OFF` or a number. A union can't be another union's arm, so `HEAD_LIMIT`'s arm of `mawsetting` is a record of the `head_limit` keyword and such a union, `head_limit_value`: `HEAD_LIMIT `. + #### List Type `list`. Collection type. Unlimited but for one rule: a list may not contain another list. Lists are distinct from arrays in two ways: a list element may be a composite type and a list admits sparse representations. diff --git a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py index d753d46b..5223aca0 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -643,6 +643,122 @@ def _fix_cellid_fields(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.B return {**blocks, block_name: new_block} +def _replace_named( + name: str, + blocks: dict[str, v2.Block], + block_name: str, + replace: Callable[[Any, bool], Any], + names: set[str], +) -> dict[str, v2.Block]: + """Replace each field in ``block_name`` (at any depth) named in ``names`` + with ``replace(field, in_union)``, ``in_union`` saying whether it's a + union's arm. Raise if the block or any of the names is missing, so a + fixup table can't silently go stale when the DFNs change.""" + found: set[str] = set() + + def _walk(field: Any, in_union: bool = False) -> Any: + if field.name in names and not isinstance(field, (v2.Record, v2.List)): + found.add(field.name) + return replace(field, in_union) + if isinstance(field, v2.Record): + return field.model_copy( + update={"fields": {n: _walk(f) for n, f in field.fields.items()}} + ) + if isinstance(field, v2.Union): + arms = [_walk(f, True) for f in field.arms.values()] + return field.model_copy(update={"arms": {f.name: f for f in arms}}) + if isinstance(field, v2.List): + return field.model_copy(update={"item": _walk(field.item)}) + return field + + block = blocks.get(block_name) + if block is None: + raise ValueError(f"{name}: no block {block_name!r}") + fields = {n: _walk(f) for n, f in block.fields.items()} + if missing := names - found: + raise ValueError(f"{name}: no field(s) {sorted(missing)} in block {block_name!r}") + return {**blocks, block_name: block.model_copy(update={"fields": fields})} + + +# v1 fields typed `string` that MF6 reads as a number, or as the given word +# (`gwf-lak.f90`'s `case ('NONE')`, `gwf-maw.f90`'s `trim(str) == 'OFF'`). v1 +# can't say "a number or this word", so the word is only in the description. +# Each becomes an untagged union of the word (a keyword, tried first) and a +# number. Entries are (block, field, word). +_NUMBER_OR_WORD: dict[str, list[tuple[str, str, str]]] = { + "gwf-lak": [("connectiondata", "bedleak", "none")], + "gwf-maw": [("period", "head_limit", "off")], # an arm of `mawsetting` +} + + +def _fix_number_or_word_fields(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + """Retype this component's number-or-word fields. See `_NUMBER_OR_WORD`. + + A union can't be a union's arm, so a tagged field that is one (MAW's + ``HEAD_LIMIT ``) becomes a record arm instead: its keyword, + then the union, named ``_value``. This is the form of the + keystring's other multi-token arms (``SHUT_OFF ``).""" + for block_name, field_name, word in _NUMBER_OR_WORD.get(name, []): + + def _replace(field: Any, in_union: bool, word: str = word) -> Any: + if not isinstance(field, v2.String): + raise ValueError(f"{name}: {field.name!r} is {field.type!r}, expected 'string'") + if in_union != field.tagged: + raise ValueError( + f"{name}: {field.name!r} is {'' if field.tagged else 'un'}tagged, " + f"expected {'a union arm' if field.tagged else 'a record column'}" + ) + data = {k: getattr(field, k) for k in v2.InputFieldBase.model_fields} + arms: dict[str, v2.Scalar | v2.Array | v2.Record] = { + word: v2.Keyword(name=word), + "value": v2.Double(name="value", tagged=False), + } + if not in_union: + return v2.Union(**{**data, "tagged": False}, arms=arms) + value = v2.Union( + **{**data, "name": f"{field.name}_value", "tagged": False, "optional": False}, + arms=arms, + ) + return v2.Record( + name=f"{field.name}record", + optional=field.optional, + fields={field.name: v2.Keyword(name=field.name), value.name: value}, + ) + + blocks = _replace_named(name, blocks, block_name, _replace, {field_name}) + return blocks + + +# v1 time-series values typed `string` rather than `double` like every other +# (`time_series` on a `double` already means "or a time series' name"). mf6 +# retyped these on develop after 6.8.1 (MODFLOW-ORG/modflow6#2786); drop the +# entry once no supported DFNs predate that. Entries are (block, fields). +_STRING_TIME_SERIES: dict[str, tuple[str, set[str]]] = { + "gwf-uzf": ("period", {"finf", "pet", "extdp", "extwc", "ha", "hroot", "rootact"}), +} + + +def _fix_string_time_series(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + """Retype this component's `string` time-series values as `double`. See + `_STRING_TIME_SERIES`.""" + entry = _STRING_TIME_SERIES.get(name) + if entry is None: + return blocks + block_name, names = entry + + def _replace(field: Any, in_union: bool) -> Any: + if isinstance(field, v2.Double) and field.time_series: + return field # already retyped upstream + if not (isinstance(field, v2.String) and field.time_series): + raise ValueError( + f"{name}: {field.name!r} is {field.type!r}, expected a time-series 'string'" + ) + shared = (set(v2.Double.model_fields) & field.model_fields_set) - {"type"} + return v2.Double(**{k: getattr(field, k) for k in shared}) + + return _replace_named(name, blocks, block_name, _replace, names) + + def _link_namefile_fields(name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: """Turn a name file's filename columns (see `_NAMEFILE_LINKS`) from Strings into input Files linking the components they name. Name-file @@ -2136,6 +2252,8 @@ def _lookup(rname: str) -> dict | None: # reads or depends on `pk`/`fk` state. blocks = _mark_lonely_pk(blocks) blocks = _fix_cellid_fields(name, blocks) + blocks = _fix_number_or_word_fields(name, blocks) + blocks = _fix_string_time_series(name, blocks) blocks = _mark_cellids(blocks) blocks = _apply_fk_backfill(name, blocks) blocks = _apply_array_fk_backfill(name, blocks)