From b4c6fc025517f65ee30acca27109054a39a3bdad Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Tue, 5 May 2026 17:15:37 -0700 Subject: [PATCH 01/29] feat(dfns): specify and reimplement v2 dfn schema --- autotest/test_dfns.py | 168 ++-- autotest/test_dfns_registry.py | 14 +- autotest/test_dfns_schema.py | 828 ++++++++++++++++ docs/index.rst | 36 +- docs/md/act.md | 21 - docs/md/dfn-schema.md | 650 +++++++++++++ docs/md/doctoc.md | 46 - docs/md/programs.md | 6 +- modflow_devtools/dfns/__init__.py | 1296 +++++++++++++++---------- modflow_devtools/dfns/dfn2toml.py | 32 +- modflow_devtools/dfns/schema/block.py | 20 - modflow_devtools/dfns/schema/field.py | 22 - modflow_devtools/dfns/schema/v1.py | 33 +- modflow_devtools/dfns/schema/v2.py | 857 +++++++++++++++- 14 files changed, 3218 insertions(+), 811 deletions(-) create mode 100644 autotest/test_dfns_schema.py delete mode 100644 docs/md/act.md create mode 100644 docs/md/dfn-schema.md delete mode 100644 docs/md/doctoc.md delete mode 100644 modflow_devtools/dfns/schema/block.py delete mode 100644 modflow_devtools/dfns/schema/field.py diff --git a/autotest/test_dfns.py b/autotest/test_dfns.py index af4b8310..a1fb7acf 100644 --- a/autotest/test_dfns.py +++ b/autotest/test_dfns.py @@ -1,4 +1,3 @@ -from dataclasses import asdict from pathlib import Path import pytest @@ -8,7 +7,17 @@ from modflow_devtools.dfns.dfn2toml import convert, is_valid from modflow_devtools.dfns.fetch import fetch_dfns from modflow_devtools.dfns.schema.v1 import FieldV1 -from modflow_devtools.dfns.schema.v2 import FieldV2 +from modflow_devtools.dfns.schema.v2 import ( + Array, + Double, + FieldBase, + FieldV2, + Integer, + Keyword, + Record, + String, + Union, +) from modflow_devtools.markers import requires_pkg PROJ_ROOT = Path(__file__).parents[1] @@ -31,13 +40,11 @@ def pytest_generate_tests(metafunc): metafunc.parametrize("dfn_name", dfn_names, ids=dfn_names) if "toml_name" in metafunc.fixturenames: - # Only convert if TOML files don't exist yet (avoid repeated conversions) dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] if not TOML_DIR.exists() or not all( (TOML_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths ): convert(DFN_DIR, TOML_DIR) - # Verify all expected TOML files were created assert all((TOML_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) toml_names = [toml.stem for toml in TOML_DIR.glob("*.toml")] metafunc.parametrize("toml_name", toml_names, ids=toml_names) @@ -108,11 +115,11 @@ def test_convert(function_tmpdir): if gwf := models.get("gwf-nam", None): pkgs = gwf.children or {} - pkgs = {k: v for k, v in pkgs.items() if k.startswith("gwf-") and isinstance(v, dict)} + pkgs = {k: v for k, v in pkgs.items() if k.startswith("gwf-")} assert len(pkgs) > 0 if dis := pkgs.get("gwf-dis", None): assert dis.name == "gwf-dis" - assert dis.parent == "gwf" + assert dis.parent == "gwf-nam" assert "options" in (dis.blocks or {}) assert "dimensions" in (dis.blocks or {}) @@ -166,7 +173,7 @@ def test_dfn_from_dict_roundtrip(): multi=True, blocks={"options": {}}, ) - d = asdict(original) + d = original.model_dump() reconstructed = Dfn.from_dict(d) assert reconstructed.name == original.name assert reconstructed.schema_version == original.schema_version @@ -183,9 +190,9 @@ def test_fieldv1_from_dict_ignores_extra_keys(): "extra_key": "should be allowed", "another_extra": 123, } - field = FieldV1.from_dict(d) - assert field.name == "test_field" - assert field.type == "keyword" + f = FieldV1.from_dict(d) + assert f.name == "test_field" + assert f.type == "keyword" def test_fieldv1_from_dict_strict_mode(): @@ -199,6 +206,8 @@ def test_fieldv1_from_dict_strict_mode(): def test_fieldv1_from_dict_roundtrip(): + from dataclasses import asdict + original = FieldV1( name="maxbound", type="integer", @@ -222,9 +231,10 @@ def test_fieldv2_from_dict_ignores_extra_keys(): "extra_key": "should be allowed", "another_extra": 123, } - field = FieldV2.from_dict(d) - assert field.name == "test_field" - assert field.type == "keyword" + f = FieldBase.from_dict(d) + assert f.name == "test_field" + assert f.type == "keyword" + assert isinstance(f, Keyword) def test_fieldv2_from_dict_strict_mode(): @@ -234,22 +244,20 @@ def test_fieldv2_from_dict_strict_mode(): "extra_key": "should cause error", } with pytest.raises(ValueError, match="Unrecognized keys in field data"): - FieldV2.from_dict(d, strict=True) + FieldBase.from_dict(d, strict=True) def test_fieldv2_from_dict_roundtrip(): - original = FieldV2( + original = Integer( name="nper", - type="integer", - block="dimensions", description="number of stress periods", optional=False, ) - d = asdict(original) - reconstructed = FieldV2.from_dict(d) + d = original.model_dump() + reconstructed = FieldBase.from_dict(d) + assert isinstance(reconstructed, Integer) assert reconstructed.name == original.name assert reconstructed.type == original.type - assert reconstructed.block == original.block assert reconstructed.description == original.description assert reconstructed.optional == original.optional @@ -276,12 +284,12 @@ def test_dfn_from_dict_with_v1_field_dicts(): assert "options" in dfn.blocks assert "save_flows" in dfn.blocks["options"] - field = dfn.blocks["options"]["save_flows"] - assert isinstance(field, FieldV1) - assert field.name == "save_flows" - assert field.type == "keyword" - assert field.tagged is True - assert field.in_record is False + f = dfn.blocks["options"]["save_flows"] + assert isinstance(f, FieldV1) + assert f.name == "save_flows" + assert f.type == "keyword" + assert f.tagged is True + assert f.in_record is False def test_dfn_from_dict_with_v2_field_dicts(): @@ -305,11 +313,11 @@ def test_dfn_from_dict_with_v2_field_dicts(): assert "dimensions" in dfn.blocks assert "nper" in dfn.blocks["dimensions"] - field = dfn.blocks["dimensions"]["nper"] - assert isinstance(field, FieldV2) - assert field.name == "nper" - assert field.type == "integer" - assert field.optional is False + f = dfn.blocks["dimensions"]["nper"] + assert isinstance(f, Integer) + assert f.name == "nper" + assert f.type == "integer" + assert f.optional is False def test_dfn_from_dict_defaults_to_v2_fields(): @@ -326,25 +334,26 @@ def test_dfn_from_dict_defaults_to_v2_fields(): } dfn = Dfn.from_dict(d) assert dfn.blocks is not None - field = dfn.blocks["options"]["some_field"] - assert isinstance(field, FieldV2) + f = dfn.blocks["options"]["some_field"] + assert isinstance(f, Keyword) + assert isinstance(f, FieldBase) assert dfn.schema_version == Version("2") def test_dfn_from_dict_with_already_deserialized_fields(): - field = FieldV2(name="test", type="keyword") + kw = Keyword(name="test") d = { "schema_version": Version("2"), "name": "test-dfn", "blocks": { "options": { - "test": field, + "test": kw, }, }, } dfn = Dfn.from_dict(d) assert dfn.blocks is not None - assert dfn.blocks["options"]["test"] is field + assert dfn.blocks["options"]["test"] is kw @requires_pkg("boltons") @@ -383,7 +392,7 @@ def test_validate_nonexistent_file(function_tmpdir): def test_fieldv1_to_fieldv2_conversion(): - """Test that FieldV1 instances are properly converted to FieldV2.""" + """Test that FieldV1 instances are properly converted to typed v2 instances.""" from modflow_devtools.dfns import map dfn_v1 = Dfn( @@ -417,68 +426,57 @@ def test_fieldv1_to_fieldv2_conversion(): assert "save_flows" in dfn_v2.blocks["options"] save_flows = dfn_v2.blocks["options"]["save_flows"] - assert isinstance(save_flows, FieldV2) + assert isinstance(save_flows, Keyword) + assert isinstance(save_flows, FieldBase) assert save_flows.name == "save_flows" assert save_flows.type == "keyword" - assert save_flows.block == "options" assert save_flows.description == "save calculated flows" - assert hasattr(save_flows, "tagged") assert not hasattr(save_flows, "in_record") assert not hasattr(save_flows, "reader") some_float = dfn_v2.blocks["options"]["some_float"] - assert isinstance(some_float, FieldV2) + assert isinstance(some_float, Double) assert some_float.name == "some_float" assert some_float.type == "double" - assert some_float.block == "options" assert some_float.description == "a floating point value" def test_fieldv1_to_fieldv2_conversion_with_children(): - """Test that FieldV1 with nested children are properly converted to FieldV2.""" + """Test that FieldV1 with nested children are properly converted to typed v2 instances.""" from modflow_devtools.dfns import map - # Create nested fields for a record - child_field_v1 = FieldV1( - name="cellid", - type="integer", - block="period", - description="cell identifier", - in_record=True, - tagged=False, - ) - - parent_field_v1 = FieldV1( - name="stress_period_data", - type="recarray cellid", - block="period", - description="stress period data", - in_record=False, - ) - dfn_v1 = Dfn( schema_version=Version("1"), name="test-dfn", blocks={ "period": { - "stress_period_data": parent_field_v1, - "cellid": child_field_v1, + "stress_period_data": FieldV1( + name="stress_period_data", + type="recarray cellid", + block="period", + description="stress period data", + in_record=False, + ), + "cellid": FieldV1( + name="cellid", + type="integer", + block="period", + description="cell identifier", + in_record=True, + tagged=False, + ), } }, ) - # Convert to v2 dfn_v2 = map(dfn_v1, schema_version="2") - - # Check that all fields are FieldV2 instances assert dfn_v2.blocks is not None - for block_name, block_fields in dfn_v2.blocks.items(): - for field_name, field in block_fields.items(): - assert isinstance(field, FieldV2) - # Check nested children too - if field.children: - for child_name, child_field in field.children.items(): - assert isinstance(child_field, FieldV2) + for block_fields in dfn_v2.blocks.values(): + for f in block_fields.values(): + assert isinstance(f, FieldBase) + if f.children: + for child in f.children.values(): + assert isinstance(child, FieldBase) def test_period_block_conversion(): @@ -517,13 +515,13 @@ def test_period_block_conversion(): dfn_v2 = map(dfn_v1, schema_version="2") period_block = dfn_v2.blocks["period"] - assert "cellid" not in period_block # cellid removed + assert "cellid" not in period_block assert "q" in period_block - assert isinstance(period_block["q"], FieldV2) - # Shape should be transformed: maxbound removed, nper and nnodes added - assert "nper" in period_block["q"].shape - assert "nnodes" in period_block["q"].shape - assert "maxbound" not in period_block["q"].shape + q = period_block["q"] + assert isinstance(q, Array) + assert "nper" in q.shape + assert "nodes" in q.shape + assert "maxbound" not in q.shape def test_record_type_conversion(): @@ -560,17 +558,17 @@ def test_record_type_conversion(): dfn_v2 = map(dfn_v1, schema_version="2") auxrecord = dfn_v2.blocks["options"]["auxrecord"] - assert isinstance(auxrecord, FieldV2) + assert isinstance(auxrecord, Record) assert auxrecord.type == "record" assert auxrecord.children is not None assert "auxiliary" in auxrecord.children assert "auxname" in auxrecord.children - assert isinstance(auxrecord.children["auxiliary"], FieldV2) - assert isinstance(auxrecord.children["auxname"], FieldV2) + assert isinstance(auxrecord.children["auxiliary"], Keyword) + assert isinstance(auxrecord.children["auxname"], String) def test_keystring_type_conversion(): - """Test keystring type conversion.""" + """Test keystring (union) type conversion.""" from modflow_devtools.dfns import map dfn_v1 = Dfn( @@ -610,7 +608,7 @@ def test_keystring_type_conversion(): dfn_v2 = map(dfn_v1, schema_version="2") obs_rec = dfn_v2.blocks["options"]["obs_filerecord"] - assert isinstance(obs_rec, FieldV2) + assert isinstance(obs_rec, Record) assert obs_rec.type == "record" assert obs_rec.children is not None - assert all(isinstance(child, FieldV2) for child in obs_rec.children.values()) + assert all(isinstance(child, FieldBase) for child in obs_rec.children.values()) diff --git a/autotest/test_dfns_registry.py b/autotest/test_dfns_registry.py index f9c3a7e2..7bc130d0 100644 --- a/autotest/test_dfns_registry.py +++ b/autotest/test_dfns_registry.py @@ -131,14 +131,12 @@ def test_hierarchical_access(self, dfn_dir): # Root should be sim-nam assert spec.root.name == "sim-nam" - # Root should have children - assert spec.root.children is not None - assert "gwf-nam" in spec.root.children - - # gwf-nam should have its own children - gwf_nam = spec.root.children["gwf-nam"] - assert gwf_nam.children is not None - assert "gwf-chd" in gwf_nam.children + # children_of is the query API; components carry parent, not children + root_children = spec.children_of("sim-nam") + assert "gwf-nam" in root_children + + gwf_nam_children = spec.children_of("gwf-nam") + assert "gwf-chd" in gwf_nam_children def test_load_empty_directory_raises(self, tmp_path): """Test that loading from empty directory raises ValueError.""" diff --git a/autotest/test_dfns_schema.py b/autotest/test_dfns_schema.py new file mode 100644 index 00000000..762d7053 --- /dev/null +++ b/autotest/test_dfns_schema.py @@ -0,0 +1,828 @@ +""" +Tests for v2 schema types, DfnSpec dimension resolution, and Array shape validation. +""" + +import pytest + +from modflow_devtools.dfns.schema.v2 import ( + Array, + Block, + ComponentBase, + DfnSpec, + Double, + Integer, + Keyword, + List, + Model, + Package, + Record, + Simulation, + String, + Union, + _collect_explicit_dims, + _known_dims_for, + _names_in_expr, + _resolve_derived_dims, + _validate_fk_fields, + _validate_shape_element, + _validate_sum_call, +) + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + + +def _dim_block(*names: str) -> Block: + """Build a dimensions Block with the named Integer dimension fields.""" + return Block( + name="dimensions", + fields={n: Integer(name=n, dimension=True) for n in names}, + ) + + +def _pkg(name: str, blocks=None, derived_dims=None, parent=None, **kw) -> Package: + return Package( + name=name, blocks=blocks, derived_dims=derived_dims, parent=parent, **kw + ) + + +# ── _collect_explicit_dims ──────────────────────────────────────────────────── + + +def test_collect_explicit_dims_basic(): + block = _dim_block("nlay", "nrow", "ncol") + pkg = _pkg("gwf-dis", blocks={"dimensions": block}) + assert _collect_explicit_dims(pkg) == {"nlay", "nrow", "ncol"} + + +def test_collect_explicit_dims_ignores_non_dimension_integers(): + block = Block( + name="options", + fields={ + "maxbound": Integer(name="maxbound", dimension=False), + "nlay": Integer(name="nlay", dimension=True), + }, + ) + pkg = _pkg("test", blocks={"options": block}) + assert _collect_explicit_dims(pkg) == {"nlay"} + + +def test_collect_explicit_dims_ignores_non_integer_fields(): + block = Block( + name="dimensions", + fields={ + "nlay": Integer(name="nlay", dimension=True), + "name": String(name="name"), + }, + ) + pkg = _pkg("test", blocks={"dimensions": block}) + assert _collect_explicit_dims(pkg) == {"nlay"} + + +def test_collect_explicit_dims_empty_when_no_blocks(): + pkg = _pkg("test", blocks=None) + assert _collect_explicit_dims(pkg) == set() + + +def test_collect_explicit_dims_across_multiple_blocks(): + b1 = Block(name="dimensions", fields={"nlay": Integer(name="nlay", dimension=True)}) + b2 = Block(name="griddata", fields={"ncol": Integer(name="ncol", dimension=True)}) + pkg = _pkg("test", blocks={"dimensions": b1, "griddata": b2}) + assert _collect_explicit_dims(pkg) == {"nlay", "ncol"} + + +# ── _names_in_expr ──────────────────────────────────────────────────────────── + + +def test_names_in_expr_simple_arithmetic(): + assert _names_in_expr("nlay * nrow * ncol") == {"nlay", "nrow", "ncol"} + + +def test_names_in_expr_single_name(): + assert _names_in_expr("nodes") == {"nodes"} + + +def test_names_in_expr_excludes_sum_internals(): + names = _names_in_expr("sum(packagedata.nlakeconn)") + assert "packagedata" not in names + assert "nlakeconn" not in names + + +def test_names_in_expr_mixed_sum_and_arithmetic(): + names = _names_in_expr("nlay * nrow + sum(packagedata.nlakeconn)") + assert names == {"nlay", "nrow"} + + +def test_names_in_expr_excludes_sum_func_name_itself(): + names = _names_in_expr("sum(list.col)") + assert "sum" not in names + + +def test_names_in_expr_invalid_syntax(): + with pytest.raises(ValueError, match="Invalid expression"): + _names_in_expr("nlay * (") + + +# ── _validate_sum_call ──────────────────────────────────────────────────────── + + +def _make_sum_call(expr: str): + import ast + + tree = ast.parse(expr, mode="eval") + for node in ast.walk(tree): + if isinstance(node, ast.Call): + return node + raise AssertionError("No Call node found") + + +def _pkg_with_list(list_field_name: str, col_name: str, col_type=None) -> Package: + col = (col_type or Integer)(name=col_name) + item = Record(name="item", fields={col_name: col}) + lst = List(name=list_field_name, item=item) + block = Block(name=list_field_name, fields={list_field_name: lst}) + return _pkg("test", blocks={list_field_name: block}) + + +def test_validate_sum_call_short_form(): + pkg = _pkg_with_list("packagedata", "nlakeconn") + call = _make_sum_call("sum(packagedata.nlakeconn)") + _validate_sum_call(call, pkg, "sum(packagedata.nlakeconn)") + + +def test_validate_sum_call_long_form(): + pkg = _pkg_with_list("packagedata", "nlakeconn") + call = _make_sum_call("sum(packagedata.packagedata.nlakeconn)") + _validate_sum_call(call, pkg, "sum(packagedata.packagedata.nlakeconn)") + + +def test_validate_sum_call_unknown_list(): + pkg = _pkg("test", blocks=None) + call = _make_sum_call("sum(nolist.col)") + with pytest.raises(ValueError, match="unknown list field"): + _validate_sum_call(call, pkg, "sum(nolist.col)") + + +def test_validate_sum_call_wrong_block_qualifier(): + pkg = _pkg_with_list("packagedata", "nlakeconn") + call = _make_sum_call("sum(wrongblock.packagedata.nlakeconn)") + with pytest.raises(ValueError, match="block qualifier"): + _validate_sum_call(call, pkg, "sum(wrongblock.packagedata.nlakeconn)") + + +def test_validate_sum_call_non_integer_column(): + pkg = _pkg_with_list("packagedata", "name", col_type=String) + call = _make_sum_call("sum(packagedata.name)") + with pytest.raises(ValueError, match="must be Integer"): + _validate_sum_call(call, pkg, "sum(packagedata.name)") + + +def test_validate_sum_call_missing_column(): + pkg = _pkg_with_list("packagedata", "nlakeconn") + call = _make_sum_call("sum(packagedata.nosuchcol)") + with pytest.raises(ValueError, match="not found"): + _validate_sum_call(call, pkg, "sum(packagedata.nosuchcol)") + + +# ── _resolve_derived_dims ───────────────────────────────────────────────────── + + +def test_resolve_derived_dims_single(): + block = _dim_block("nlay", "nrow", "ncol") + pkg = _pkg("test", blocks={"dimensions": block}, derived_dims={"nodes": "nlay * nrow * ncol"}) + order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) + assert order == ["nodes"] + + +def test_resolve_derived_dims_chain(): + block = _dim_block("nlay", "nrow", "ncol") + pkg = _pkg( + "test", + blocks={"dimensions": block}, + derived_dims={"nodes": "nlay * nrow * ncol", "nodouble": "nodes * 2"}, + ) + order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) + assert order.index("nodes") < order.index("nodouble") + + +def test_resolve_derived_dims_inherited_dim_operand_allowed(): + # A dim from another component (e.g. "nodes" from gwf-dis) is passed in via + # known_dims and should be accepted as a valid derived-dim operand. + pkg = _pkg("test", blocks=None, derived_dims={"derived": "nodes + 1"}) + order = _resolve_derived_dims(pkg, {"nodes"}) + assert order == ["derived"] + + +def test_resolve_derived_dims_sum_operand_allowed(): + pkg = _pkg_with_list("packagedata", "nlakeconn") + pkg = Package( + name="test", + blocks=pkg.blocks, + derived_dims={"total_conn": "sum(packagedata.nlakeconn)"}, + ) + order = _resolve_derived_dims(pkg, set()) + assert order == ["total_conn"] + + +def test_resolve_derived_dims_no_derived_returns_empty(): + pkg = _pkg("test", blocks=None, derived_dims=None) + assert _resolve_derived_dims(pkg, set()) == [] + + +def test_resolve_derived_dims_cycle_error(): + pkg = _pkg("test", blocks=None, derived_dims={"a": "b + 1", "b": "a + 1"}) + with pytest.raises(ValueError, match="Cycle in derived_dims"): + _resolve_derived_dims(pkg, set()) + + +def test_resolve_derived_dims_unknown_operand_error(): + pkg = _pkg("test", blocks=None, derived_dims={"nodes": "mystery_dim * 2"}) + with pytest.raises(ValueError, match="not a known dimension"): + _resolve_derived_dims(pkg, set()) + + +def test_resolve_derived_dims_invalid_expression_error(): + pkg = _pkg("test", blocks=None, derived_dims={"nodes": "nlay * ("}) + with pytest.raises(ValueError, match="Invalid derived_dims"): + _resolve_derived_dims(pkg, set()) + + +# ── DfnSpec construction and validation ─────────────────────────────────────── + + +def test_dfnspec_construction_validates_dims(): + block = _dim_block("nlay", "nrow", "ncol") + pkg = _pkg( + "gwf-dis", + blocks={"dimensions": block}, + derived_dims={"nodes": "nlay * nrow * ncol"}, + ) + spec = DfnSpec(components={"gwf-dis": pkg}) + assert "gwf-dis" in spec + + +def test_dfnspec_construction_cycle_raises(): + pkg = _pkg("bad", blocks=None, derived_dims={"a": "b + 1", "b": "a + 1"}) + with pytest.raises(ValueError, match="Cycle in derived_dims"): + DfnSpec(components={"bad": pkg}) + + +def test_dfnspec_construction_unknown_operand_raises(): + pkg = _pkg("bad", blocks=None, derived_dims={"nodes": "ghost_dim * 2"}) + with pytest.raises(ValueError, match="not a known dimension"): + DfnSpec(components={"bad": pkg}) + + +def test_dfnspec_no_derived_dims_constructs_fine(): + pkg = _pkg("gwf-chd", blocks=None, derived_dims=None) + spec = DfnSpec(components={"gwf-chd": pkg}) + assert "gwf-chd" in spec + + +# ── DfnSpec.explicit_dims_for ───────────────────────────────────────────────── + + +def test_dfnspec_explicit_dims_for(): + block = _dim_block("nlay", "nrow", "ncol") + pkg = _pkg("gwf-dis", blocks={"dimensions": block}) + spec = DfnSpec(components={"gwf-dis": pkg}) + assert spec.explicit_dims_for("gwf-dis") == {"nlay", "nrow", "ncol"} + + +def test_dfnspec_explicit_dims_for_empty(): + pkg = _pkg("gwf-chd", blocks=None) + spec = DfnSpec(components={"gwf-chd": pkg}) + assert spec.explicit_dims_for("gwf-chd") == set() + + +# ── DfnSpec.grid_dims_for ───────────────────────────────────────────────────── + + +def test_dfnspec_grid_dims_for_no_parent_returns_namespace(): + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + + pkg = _pkg("sim-nam", blocks=None, parent=None) + spec = DfnSpec(components={"sim-nam": pkg}) + result = spec.grid_dims_for("sim-nam") + assert result == set(GRID_DIM_NAMESPACE) + + +def test_dfnspec_grid_dims_for_includes_dis_dims(): + dis_block = _dim_block("nlay", "nrow", "ncol") + dis = _pkg("gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) + + grid_dims = spec.grid_dims_for("gwf-chd") + assert "nlay" in grid_dims + assert "nrow" in grid_dims + assert "ncol" in grid_dims + assert "nodes" in grid_dims # from GRID_DIM_NAMESPACE + + +def test_dfnspec_grid_dims_for_disv(): + disv_block = _dim_block("nlay", "ncpl") + disv = _pkg("gwf-disv", parent="gwf-nam", blocks={"dimensions": disv_block}) + chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-disv": disv, "gwf-chd": chd}) + + grid_dims = spec.grid_dims_for("gwf-chd") + assert "nlay" in grid_dims + assert "ncpl" in grid_dims + + +def test_dfnspec_grid_dims_for_disu(): + disu_block = _dim_block("nodes", "nja") + disu = _pkg("gwf-disu", parent="gwf-nam", blocks={"dimensions": disu_block}) + chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-disu": disu, "gwf-chd": chd}) + + grid_dims = spec.grid_dims_for("gwf-chd") + assert "nodes" in grid_dims + assert "nja" in grid_dims + + +def test_dfnspec_grid_dims_for_non_dis_siblings_excluded(): + dis_block = _dim_block("nlay", "nrow", "ncol") + dis = _pkg("gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + + other_block = Block( + name="dimensions", + fields={"secret_dim": Integer(name="secret_dim", dimension=True)}, + ) + other = _pkg("gwf-chd", parent="gwf-nam", blocks={"dimensions": other_block}) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": other}) + + grid_dims = spec.grid_dims_for("gwf-chd") + assert "nlay" in grid_dims + assert "secret_dim" not in grid_dims + + +# ── DfnSpec Mapping protocol ────────────────────────────────────────────────── + + +def test_dfnspec_mapping_getitem(): + pkg = _pkg("gwf-chd", parent="gwf-nam") + spec = DfnSpec(components={"gwf-chd": pkg}) + assert spec["gwf-chd"] is pkg + + +def test_dfnspec_mapping_iter(): + pkg = _pkg("gwf-chd", parent="gwf-nam") + spec = DfnSpec(components={"gwf-chd": pkg}) + assert list(spec) == ["gwf-chd"] + + +def test_dfnspec_mapping_len(): + pkgs = {f"gwf-p{i}": _pkg(f"gwf-p{i}") for i in range(3)} + spec = DfnSpec(components=pkgs) + assert len(spec) == 3 + + +def test_dfnspec_mapping_contains(): + pkg = _pkg("gwf-chd") + spec = DfnSpec(components={"gwf-chd": pkg}) + assert "gwf-chd" in spec + assert "gwf-rch" not in spec + + +# ── DfnSpec.schema_version ──────────────────────────────────────────────────── + + +def test_dfnspec_schema_version_from_component(): + from packaging.version import Version + + pkg = Package(name="gwf-chd", schema_version=Version("2")) + spec = DfnSpec(components={"gwf-chd": pkg}) + assert spec.schema_version == Version("2") + + +def test_dfnspec_schema_version_default(): + from packaging.version import Version + + pkg = _pkg("gwf-chd") + spec = DfnSpec(components={"gwf-chd": pkg}) + assert spec.schema_version == Version("2") + + +# ── DfnSpec.children_of ─────────────────────────────────────────────────────── + + +def test_dfnspec_children_of(): + gwf = Model(name="gwf-nam", blocks=None) + chd = _pkg("gwf-chd", parent="gwf-nam") + rch = _pkg("gwf-rch", parent="gwf-nam") + sim = Simulation(name="sim-nam", blocks=None) + spec = DfnSpec( + components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch} + ) + children = spec.children_of("gwf-nam") + assert set(children) == {"gwf-chd", "gwf-rch"} + + +def test_dfnspec_children_of_empty(): + pkg = _pkg("gwf-chd", parent="gwf-nam") + spec = DfnSpec(components={"gwf-chd": pkg}) + assert spec.children_of("gwf-chd") == {} + + +# ── Shape validation helpers ────────────────────────────────────────────────── + + +def _dis_spec() -> DfnSpec: + """A minimal gwf-dis + gwf-nam DfnSpec used as shared fixture scaffolding.""" + dis_block = _dim_block("nlay", "nrow", "ncol") + gwf = Model(name="gwf-nam", blocks=None) + dis = Package(name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + return DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + + +def _lake_spec(period_item: Record) -> DfnSpec: + """ + DfnSpec with a gwf-lak that has a packagedata list block and a + period list block whose item is `period_item`. + """ + nlakeconn = Integer(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + period_list = List(name="period", item=period_item) + period_block = Block(name="period", fields={"period": period_list}) + gwf = Model(name="gwf-nam", blocks=None) + lak = Package( + name="gwf-lak", + parent="gwf-nam", + blocks={"packagedata": pkg_block, "period": period_block}, + ) + return DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + + +# ── _known_dims_for ─────────────────────────────────────────────────────────── + + +def test_known_dims_includes_explicit(): + spec = _dis_spec() + known = _known_dims_for(spec, "gwf-dis") + assert {"nlay", "nrow", "ncol"} <= known + + +def test_known_dims_includes_derived(): + dis_block = _dim_block("nlay", "nrow", "ncol") + gwf = Model(name="gwf-nam", blocks=None) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block}, + derived_dims={"nodes": "nlay * nrow * ncol"}, + ) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + known = _known_dims_for(spec, "gwf-dis") + assert "nodes" in known + + +def test_known_dims_includes_grid_dims(): + spec = _dis_spec() + # gwf-chd has no local dims but inherits grid dims via gwf-dis sibling + chd = _pkg("gwf-chd", parent="gwf-nam") + spec2 = DfnSpec(components=dict(spec.components) | {"gwf-chd": chd}) + known = _known_dims_for(spec2, "gwf-chd") + assert "nodes" in known # GRID_DIM_NAMESPACE + assert "nlay" in known # from gwf-dis (sibling dis package) + + +# ── _validate_shape_element: dim reference ──────────────────────────────────── + + +def _make_ctx(dim_names: set[str], derived: dict | None = None): + """Return (array, component, known_dims) for shape element tests.""" + dis_block = _dim_block(*dim_names) + pkg = _pkg("test", blocks={"dimensions": dis_block}, derived_dims=derived) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "test": pkg}) + known = _known_dims_for(spec, "test") + arr = Array(name="arr", dtype="double", shape=[]) + return arr, pkg, known + + +def test_shape_element_valid_explicit_dim(): + arr, pkg, known = _make_ctx({"nlay", "nrow", "ncol"}) + _validate_shape_element("nlay", arr, pkg, None, known) # no error + + +def test_shape_element_valid_grid_dim(): + arr, pkg, known = _make_ctx(set()) + # "nodes" is always in GRID_DIM_NAMESPACE → known + _validate_shape_element("nodes", arr, pkg, None, known) + + +def test_shape_element_valid_derived_dim(): + arr, pkg, known = _make_ctx( + {"nlay", "nrow", "ncol"}, derived={"nodes": "nlay * nrow * ncol"} + ) + _validate_shape_element("nodes", arr, pkg, None, known) + + +def test_shape_element_unknown_dim_raises(): + arr, pkg, known = _make_ctx({"nlay"}) + with pytest.raises(ValueError, match="does not resolve"): + _validate_shape_element("mystery", arr, pkg, None, known) + + +def test_shape_element_invalid_syntax_raises(): + arr, pkg, known = _make_ctx({"nlay"}) + with pytest.raises(ValueError, match="invalid shape element"): + _validate_shape_element("123bad", arr, pkg, None, known) + + +def test_shape_element_empty_string_raises(): + arr, pkg, known = _make_ctx({"nlay"}) + with pytest.raises(ValueError, match="invalid shape element"): + _validate_shape_element("", arr, pkg, None, known) + + +# ── _validate_shape_element: row-level lookup ───────────────────────────────── + + +def _lookup_ctx(): + """ + Returns (array, enclosing_record, component, known_dims) for a valid + row-level lookup scenario mirroring the gwf-lak period table. + + packagedata block has a List with item Record(lakeno pk, nlakeconn int). + The array lives inside a Record with sibling lakeno(fk='packagedata'). + """ + # packagedata list + nlakeconn = Integer(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + + # enclosing record with fk sibling + array + fk_lakeno = Integer(name="lakeno", fk="packagedata") + arr = Array(name="outflow", dtype="double", shape=[]) + enc_record = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) + + lak = Package( + name="gwf-lak", + parent="gwf-nam", + blocks={"packagedata": pkg_block}, + ) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + known = _known_dims_for(spec, "gwf-lak") + return arr, enc_record, lak, known + + +def test_shape_element_valid_row_level_lookup(): + arr, enc, pkg, known = _lookup_ctx() + _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, pkg, enc, known) + + +def test_shape_element_lookup_on_top_level_array_raises(): + arr, enc, pkg, known = _lookup_ctx() + with pytest.raises(ValueError, match="not inside a record"): + _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, pkg, None, known) + + +def test_shape_element_lookup_unknown_list_block_raises(): + arr, enc, pkg, known = _lookup_ctx() + with pytest.raises(ValueError, match="not a list block"): + _validate_shape_element("noblock.nlakeconn(lakeno)", arr, pkg, enc, known) + + +def test_shape_element_lookup_unknown_column_raises(): + arr, enc, pkg, known = _lookup_ctx() + with pytest.raises(ValueError, match="is not a field"): + _validate_shape_element("packagedata.nocol(lakeno)", arr, pkg, enc, known) + + +def test_shape_element_lookup_non_integer_column_raises(): + # Replace nlakeconn with a String column + nlakeconn = String(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + fk_lakeno = Integer(name="lakeno", fk="packagedata") + arr = Array(name="outflow", dtype="double", shape=[]) + enc = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) + lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + known = _known_dims_for(spec, "gwf-lak") + with pytest.raises(ValueError, match="must be Integer"): + _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) + + +def test_shape_element_lookup_missing_fk_sibling_raises(): + arr, enc, pkg, known = _lookup_ctx() + with pytest.raises(ValueError, match="not a sibling field"): + _validate_shape_element("packagedata.nlakeconn(nosuchfield)", arr, pkg, enc, known) + + +def test_shape_element_lookup_fk_not_set_raises(): + # lakeno has no fk attribute set + nlakeconn = Integer(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + no_fk_lakeno = Integer(name="lakeno") # fk=None + arr = Array(name="outflow", dtype="double", shape=[]) + enc = Record(name="item", fields={"lakeno": no_fk_lakeno, "outflow": arr}) + lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + known = _known_dims_for(spec, "gwf-lak") + with pytest.raises(ValueError, match=r"\.fk is not set"): + _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) + + +def test_shape_element_lookup_fk_block_mismatch_raises(): + # packagedata block exists (check 1 passes) but fk field points to 'otherblock' + nlakeconn = Integer(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + fk_lakeno = Integer(name="lakeno", fk="otherblock") # fk → wrong block + arr = Array(name="outflow", dtype="double", shape=[]) + enc = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) + lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + known = _known_dims_for(spec, "gwf-lak") + with pytest.raises(ValueError, match="does not reference block"): + _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) + + +# ── DfnSpec shape validation end-to-end ────────────────────────────────────── + + +def test_dfnspec_valid_top_level_array_shape(): + dis_block = _dim_block("nlay", "nrow", "ncol") + arr = Array(name="botm", dtype="double", shape=["nlay", "nrow", "ncol"]) + grid_block = Block(name="griddata", fields={"botm": arr}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block, "griddata": grid_block}, + ) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + assert "gwf-dis" in spec + + +def test_dfnspec_valid_array_in_record(): + dis_block = _dim_block("nlay", "nrow", "ncol") + arr = Array(name="vals", dtype="double", shape=["ncol"]) + rec = Record(name="myrec", fields={"vals": arr}) + opt_block = Block(name="options", fields={"myrec": rec}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block, "options": opt_block}, + ) + gwf = Model(name="gwf-nam", blocks=None) + DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + + +def test_dfnspec_valid_row_level_lookup_in_list_item(): + nlakeconn = Integer(name="nlakeconn") + lakeno_pk = Integer(name="lakeno", pk=True) + pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + + fk_lakeno = Integer(name="lakeno", fk="packagedata") + outflow = Array(name="outflow", dtype="double", shape=["packagedata.nlakeconn(lakeno)"]) + period_item = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": outflow}) + period_list = List(name="period", item=period_item) + period_block = Block(name="period", fields={"period": period_list}) + + gwf = Model(name="gwf-nam", blocks=None) + lak = Package( + name="gwf-lak", + parent="gwf-nam", + blocks={"packagedata": pkg_block, "period": period_block}, + ) + DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + + +def test_dfnspec_invalid_array_shape_raises(): + dis_block = _dim_block("nlay", "nrow", "ncol") + arr = Array(name="botm", dtype="double", shape=["nlay", "no_such_dim"]) + grid_block = Block(name="griddata", fields={"botm": arr}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block, "griddata": grid_block}, + ) + gwf = Model(name="gwf-nam", blocks=None) + with pytest.raises(ValueError, match="does not resolve"): + DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + + +def test_dfnspec_array_shape_resolves_via_derived_dim(): + dis_block = _dim_block("nlay", "nrow", "ncol") + arr = Array(name="botm", dtype="double", shape=["nodes"]) + grid_block = Block(name="griddata", fields={"botm": arr}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block, "griddata": grid_block}, + derived_dims={"nodes": "nlay * nrow * ncol"}, + ) + gwf = Model(name="gwf-nam", blocks=None) + DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + + +def test_dfnspec_array_shape_resolves_via_sibling_dis(): + """An array in gwf-chd can reference nlay from sibling gwf-dis.""" + dis_block = _dim_block("nlay", "nrow", "ncol") + dis = Package(name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + chd_arr = Array(name="head", dtype="double", shape=["nlay", "nodes"]) + chd_block = Block(name="period", fields={"head": chd_arr}) + chd = Package(name="gwf-chd", parent="gwf-nam", blocks={"period": chd_block}) + gwf = Model(name="gwf-nam", blocks=None) + DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) + + +# ── _validate_fk_fields ─────────────────────────────────────────────────────── + + +def _fk_pkg_and_spec(fk_val, pk_on_item=True, fk_ref=None): + """ + Build a Package with a packagedata list block and a period block whose + item record has a lakeno field with fk=fk_val (and optionally fk_ref). + """ + nlakeconn = Integer(name="nlakeconn") + lakeno_item = Integer(name="lakeno", pk=pk_on_item) + pkg_item = Record(name="item", fields={"lakeno": lakeno_item, "nlakeconn": nlakeconn}) + pkg_list = List(name="packagedata", item=pkg_item) + pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) + + fk_field = Integer(name="lakeno", fk=fk_val, fk_ref=fk_ref) + period_item = Record(name="item", fields={"lakeno": fk_field}) + period_list = List(name="period", item=period_item) + period_block = Block(name="period", fields={"period": period_list}) + + gwf = Model(name="gwf-nam", blocks=None) + lak = Package( + name="gwf-lak", + parent="gwf-nam", + blocks={"packagedata": pkg_block, "period": period_block}, + ) + return lak, gwf + + +def test_validate_fk_fields_valid(): + lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + assert "gwf-lak" in spec + + +def test_validate_fk_fields_unknown_block_raises(): + lak, gwf = _fk_pkg_and_spec("nosuchblock", pk_on_item=True) + with pytest.raises(ValueError, match="is not a list block"): + DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + + +def test_validate_fk_fields_no_pk_on_item_raises(): + lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=False) + with pytest.raises(ValueError, match="has no pk=True field"): + DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + + +def test_validate_fk_fields_fk_ref_valid(): + lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True, fk_ref="gwf-nam") + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + assert "gwf-lak" in spec + + +def test_validate_fk_fields_fk_ref_unknown_raises(): + lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True, fk_ref="no-such-comp") + with pytest.raises(ValueError, match="not found in spec"): + DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + + +def test_validate_fk_fields_no_fk_set_passes(): + item = Record(name="item", fields={"val": Double(name="val")}) + lst = List(name="data", item=item) + block = Block(name="data", fields={"data": lst}) + pkg = Package(name="gwf-test", blocks={"data": block}) + gwf = Model(name="gwf-nam", blocks=None) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-test": pkg}) + assert "gwf-test" in spec + + +def test_validate_fk_fields_called_directly(): + lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True) + spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + _validate_fk_fields(lak, spec) # should not raise diff --git a/docs/index.rst b/docs/index.rst index c5e21c12..5f3b08f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,40 +10,46 @@ The `modflow-devtools` package provides a set of tools for developing and testin .. toctree:: :maxdepth: 2 - :caption: Introduction + :caption: Installation md/install.md - .. toctree:: :maxdepth: 2 - :caption: Test fixtures + :caption: Testing md/fixtures.md md/markers.md md/snapshots.md - .. toctree:: :maxdepth: 2 - :caption: Miscellaneous + :caption: Specification + md/dfn-schema.md md/dfns.md - md/download.md - md/latex.md - md/models.md - md/ostags.md - md/programs.md - md/timed.md - md/zip.md +.. toctree:: + :maxdepth: 2 + :caption: Models + + md/models.md .. toctree:: :maxdepth: 2 - :caption: External tools + :caption: Programs + + md/programs.md + md/ostags.md - md/act.md - md/doctoc.md +.. toctree:: + :maxdepth: 2 + :caption: Miscellaneous + + md/zip.md + md/download.md + md/timed.md + md/latex.md .. toctree:: :maxdepth: 2 diff --git a/docs/md/act.md b/docs/md/act.md deleted file mode 100644 index bee9d9cc..00000000 --- a/docs/md/act.md +++ /dev/null @@ -1,21 +0,0 @@ -# Testing CI workflows locally - -The [`act`](https://github.com/nektos/act) tool uses Docker to run CI workflows in a simulated GitHub Actions environment. [Docker Desktop](https://www.docker.com/products/docker-desktop/) is required for Mac or Windows and [Docker Engine](https://docs.docker.com/engine/) on Linux. - -**Note:** `act` can only run Linux-based container definitions. Mac or Windows workflows or matrix OS entries will be skipped. - -With Docker installed and running, run `act -l` from the project root to see available CI workflows. To run all workflows and jobs, just run `act`. To run a particular workflow use `-W`: - -```shell -act -W .github/workflows/commit.yml -``` - -To run a particular job within a workflow, add the `-j` option: - -```shell -act -W .github/workflows/commit.yml -j build -``` - -**Note:** GitHub API rate limits are easy to exceed, especially with job matrices. Authenticated GitHub users have a much higher rate limit: use `-s GITHUB_TOKEN=` when invoking `act` to provide a personal access token. Note that this will log your token in shell history — leave the value blank for a prompt to enter it more securely. - -The `-n` flag can be used to execute a dry run, which doesn't run anything, just evaluates workflow, job and step definitions. See the [docs](https://github.com/nektos/act#example-commands) for more. diff --git a/docs/md/dfn-schema.md b/docs/md/dfn-schema.md new file mode 100644 index 00000000..145f7085 --- /dev/null +++ b/docs/md/dfn-schema.md @@ -0,0 +1,650 @@ +# DFN specification + +- [Overview](#overview) +- [Components](#components) + - [Shared attributes](#shared-attributes) + - [`type`](#type) + - [`name`](#name) + - [`blocks`](#blocks) + - [`parent`](#parent) + - [`schema_version`](#schema_version) + - [Component types](#component-types) + - [Simulation](#simulation) + - [Model](#model) + - [Type-specific attributes](#type-specific-attributes) + - [`solution`](#solution) + - [Package](#package) + - [Type-specific attributes](#type-specific-attributes-1) + - [`multi`](#multi) + - [`subtype`](#subtype) + - [`variant_of`](#variant_of) +- [Blocks](#blocks-1) + - [Attributes](#attributes) + - [`name`](#name-1) + - [`fields`](#fields) + - [`repeats`](#repeats) + - [`optional`](#optional) +- [Fields](#fields-1) + - [Shared attributes](#shared-attributes-1) + - [`name`](#name-2) + - [`type`](#type-1) + - [`longname`](#longname) + - [`description`](#description) + - [`optional`](#optional-1) + - [`default`](#default) + - [`developmode`](#developmode) + - [`netcdf`](#netcdf) + - [Scalars](#scalars) + - [Keyword](#keyword) + - [String](#string) + - [Type-specific attributes](#type-specific-attributes-2) + - [`tagged`](#tagged) + - [`valid`](#valid) + - [`case_sensitive`](#case_sensitive) + - [`pk`](#pk) + - [`fk`](#fk) + - [`fk_ref`](#fk_ref) + - [Integer](#integer) + - [Type-specific attributes](#type-specific-attributes-3) + - [`tagged`](#tagged-1) + - [`valid`](#valid-1) + - [`dimension`](#dimension) + - [`time_series`](#time_series) + - [`pk`](#pk-1) + - [`fk`](#fk-1) + - [`fk_ref`](#fk_ref-1) + - [Double](#double) + - [Type-specific attributes](#type-specific-attributes-4) + - [`tagged`](#tagged-2) + - [`time_series`](#time_series-1) + - [Path](#path) + - [Type-specific attributes](#type-specific-attributes-5) + - [`mode`](#mode) + - [Composites](#composites) + - [Array](#array) + - [Type-specific attributes](#type-specific-attributes-6) + - [`dtype`](#dtype) + - [`shape`](#shape) + - [`time_series`](#time_series-2) + - [`repeat`](#repeat) + - [Record](#record) + - [Type-specific attributes](#type-specific-attributes-7) + - [`fields`](#fields-2) + - [Union](#union) + - [Type-specific attributes](#type-specific-attributes-8) + - [`arms`](#arms) + - [List](#list) + - [Type-specific attributes](#type-specific-attributes-9) + - [`item`](#item) + - [Array dimensions](#array-dimensions) + - [Derived dimensions](#derived-dimensions) + - [Row-level column lookups](#row-level-column-lookups) + - [Scope and resolution](#scope-and-resolution) + - [Primary/foreign keys](#primaryforeign-keys) + - [Examples](#examples) + +## Overview + +A MODFLOW 6 simulation consists of a hierarchy of modules, each module representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. + +This document distinguishes **modules**, conceptual units of functionality as defined in the MF6 IO guide, from **components**: particular representations of modules. + +Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component and its fields, relationships between fields or to other components, and data representations and in some cases formatting information. Component definitions should not be expected to map 1-1 to modules. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. + +## Components + +Component definitions consist primarily of a name, zero or more block definitions, as well as other optional attributes. + +- `type`: the component type (`"simulation"`, `"model"`, or `"package"`) +- `name`: the component's name +- `blocks`: block definitions +- `parent`: parent component(s) +- `schema_version`: DFN schema version + +Components may refer to, i.e. be constrained by, other components. Cross-component constraints include parent-child relations, solution compatibility, and format variants. + +### Shared attributes + +#### `type` + +The component's type. Required. One of: + +- `"simulation"`: the root of the runtime hierarchy. +- `"model"`: a hydrologic process model. +- `"package"`: a model input package. + +#### `name` + +`string`. Required. The component's name. By convention the hyphenated `abc-xyz` stem of the DFN file name. + +#### `blocks` + +`{string: Block} (default: {})`. The component's input blocks. A component may be empty, i.e. have no blocks. See section below. + +#### `parent` + +Except for the simulation, which is the root of the runtime hierarchy, all MF6 components have a parent. Parent-child relations may range from fully constrained (e.g., a `gwf-chd` package must be a child of a GWF model) to completely unconstrained. Components which may be attached to multiple possible parents are historically called **subpackages**. + +Parent relationships are defined bottom-up with attribute `parent`: + +- `null` — no parent; only valid for the root, i.e. simulation. +- `"*"` — unconstrained; any parent component type is allowed. +- A string or list of strings — declares the set of valid parent component types. Entries are either: + - **Component type names:** `"simulation"`, `"model"`, or `"package"`. + - **Concrete component names:** e.g. `"gwf-sfr"`, `"gwf-nam"` + +**Note:** Type names and concrete component names may be mixed. A type name subsumes any named component of the same type: e.g., `["gwf-sfr", "package"]` reduces to `["package"]` since `gwf-sfr` is a package. + +#### `schema_version` + +`string | null (default: null)`. The version of the DFN schema. Optional but recommended. + +### Component types + +Three component types can be distinguished: simulation, model, and package. + +#### Simulation + +The simulation is the root of the MODFLOW 6 runtime module hierarchy. A simulation may contain one or more models, organized into one or more solutions. It has no parent (`parent: null`) and no type-specific attributes beyond those shared by all components. + +#### Model + +A model represents a hydrologic process. Models are managed and solved by the simulation. + +##### Type-specific attributes + +###### `solution` + +`"ims" | "ems" | "sln-ims" | "sln-ems" | null (default: null)`. MF6 supports different solution schemes: implicit solutions (solve systems of coupled equations iteratively) and explicit solutions (used when closed-form solutions are available). A model declares which solution type it requires with the optional `solution` attribute. Solution packages do not redundantly declare which model types they support; compatibility is determined entirely from the model side. + +#### Package + +A package is any component that is not a simulation or a model. + +##### Type-specific attributes + +###### `multi` + +`boolean (default: false)`. Indicates that multiple instances of this component are permitted. Components of which multiple instances are allowed are called "multi-packages". + +###### `subtype` + +Optional discriminator indicating the package's functional role. Several package subtypes may be distinguished: solutions, exchanges, stress packages, advanced packages, and utility packages. + +- `"solution"`: provides solving capability for models. Compatibility with a model is determined by the model's `solution` attribute. +- `"exchange"`: connects two models, enabling them to share boundary conditions or state at their interface. Parent is the simulation. +- `"stress"`: imposes boundary conditions on a model. Period data is provided per stress period; each period block replaces the full set of stresses for that period. +- `"advanced"`: an advanced stress package. Differs from `"stress"` in three key ways: + 1. Solves a continuity equation. Each feature (well, reach, lake cell, UZF cell) internally balances inflows, outflows, and change in storage. Traditional stress packages impose static conditions and do not have an internal water budget. **Note:** advanced packages can act as receivers in the Water Mover (MVR/MVE) package because they have an internal continuity equation to receive diverted water into. Traditional stress packages cannot. + 2. Has dynamic state variables. Advanced packages compute a dependent variable (e.g., lake stage, well head, reach stage) that is part of the solution. Traditional stress packages use fixed/user-specified values. + 3. Stress periods have feature replacement rather than block replacement semantics: when a new period block configuration is provided, traditional stress packages replace the entire previous configuration; advanced packages perform partial updates, modifying only features explicitly appearing in the new period block. **Note:** both simple and advanced packages fill-forward across omitted stress periods; the distinction is only in what happens when a new period block configuration is specified. +- `"utility"`: an auxiliary package that may be attached to models or packages, such as time series, time-array series, or observations. Utility packages (`utl-*`) are distinguished from primary model input packages by providing configurational or cross-cutting concerns rather than representing a first-class hydrologic process. They may support `multi` and `variant_of`; they never have `subtype` `"solution"`, `"exchange"`, `"stress"`, or `"advanced"`. + +`subtype: null` (the default) covers packages that don't fall into any named category, such as output control packages. + +###### `variant_of` + +`string | null (default: null)`. Some modules may be represented by several different components: e.g., typical stress packages define period data sparsely as a list, while layer- and grid-array variants allow providing period data as arrays. + +A package may signal that it has equivalent functional semantics as another component with the `variant_of` attribute. This attribute is only meaningful on packages (including utility packages); variants of models or simulations are not supported. + +## Blocks + +A block is group of related fields, essentially a product type. Record fields are also product types; the distinction is that records occupy a single line in MF6 input files, while blocks are multiline constructs delimited by headers, e.g. + +``` +begin + field1 value + field2 value +end +``` + +Blocks are treated differently depending on the structural composition of their top-level fields. The sample above is typical of a block containing configuration options, which is essentially a dictionary mapping field names to values. + +A field's value need not be preceded by its name; see the `tagged` section below. Tagged fields must precede any and all untagged fields in the block definition and consequently in input files. + +### Attributes + +#### `name` + +`string`. Required. The identifier used in `begin ` / `end ` delimiters in MF6 input files. + +#### `fields` + +`{string: Field} (default: {})`. The block's fields, in definition order. + +#### `repeats` + +`boolean (default: false)`. Whether the block may appear multiple times in an input file. When true, each occurrence is read independently, and associated with a unique label. The canonical repeating block is the period block, whose label is the stress period number. + +#### `optional` + +`boolean (default: false)`. Whether the block may be omitted entirely from the input file. An absent optional block is treated as empty. + +## Fields + +A field is a [tagged union](https://en.wikipedia.org/wiki/Tagged_union) of concrete data types, discriminated by a `type` attribute. A field consists of a set of attributes, some shared, some type-specific. + +Field definitions are not entirely self-contained. Some fields may refer to other fields, in the same component or in another. There are two cases of this: + +- array dimensions +- list primary/foreign keys + +These cases are associated with type-specific attributes described below. + +### Shared attributes + +There is a core set of attributes shared by all field types: + +- `name` +- `type` +- `longname` +- `description` +- `optional` +- `default` +- `developmode` +- `netcdf` + +#### `name` + +`string`. Required. + +#### `type` + +Required. One of: + +- `keyword` +- `integer` +- `double` +- `array` +- `string` +- `path` +- `record` +- `union` +- `list` + +#### `longname` + +`string | null (default: null)`. A longer, more descriptive name. From the [NetCDF conventions](https://docs.unidata.ucar.edu/nug/current/attribute_conventions.html#long_name). May contain spaces. + +#### `description` + +`string | null (default: null)`. A detailed description of the field. + +#### `optional` + +`boolean (default: false)`. Indicates that the field is not mandatory and may be omitted. May be applied to both composite and scalar fields. + +#### `default` + +The field's default value. Only relevant for optional fields. TODO: determine whether to keep. MF6 doesn't read DFN defaults, only flopy does. MF6 implements defaults internally, so care must be taken to keep DFNs in sync, or maybe IDM could read the default from the DFNs. + +#### `developmode` + +`boolean (default: false)`. Feature flag indicating that the field is not released yet, only allowed in develop mode builds. + +#### `netcdf` + +`boolean (default: false)`. Marks a field that can appear in NetCDF input files. + +### Scalars + +Scalar fields define a single value. + +#### Keyword + +Type `keyword`. Represents a boolean choice. In input files, the presence of a keyword indicates true, its absence false. + +#### String + +Type `string`. + +##### Type-specific attributes + +###### `tagged` + +`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. + +###### `valid` + +`[string] | null`. Permitted values (enumeration constraint). Empty list is treated as absent. + +###### `case_sensitive` + +`boolean (default: false)`. Indicates that the string's case must be preserved. The MF6 parser uppercases strings by default. + +###### `pk` + +`boolean (default: false)`. Marks this scalar as the primary key of its containing list's item record. Valid only on integer or string scalars that are columns in a list item record. Exactly one column per list item may be marked pk. + +###### `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. Three forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) sentinel `"node"` — grid cell reference, used without `fk_ref`; (3) 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". + +###### `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". + +#### Integer + +Type `integer`. + +##### Type-specific attributes + +###### `tagged` + +`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. + +###### `valid` + +`[integer] | null`. Permitted values (enumeration constraint). Empty list is treated as absent. + +###### `dimension` + +`"record" | "component" | "model" | "simulation" | true | false | null (default: null)`. Declares this field as a dimension source and specifies its scope. See [Scope and resolution](#scope-and-resolution). A `true` value indicates `"component"` scope; `false` and `null` are equivalent. + +###### `time_series` + +`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferrable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. + +###### `pk` + +`boolean (default: false)`. Marks this scalar as the primary key of its containing list's item record. Valid only on integer or string scalars that are columns in a list item record. Exactly one column per list item may be marked pk. + +###### `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. Three forms: (1) hierarchical path `"block.field"` or `"component.block.field"` — fully static, used without `fk_ref`; (2) sentinel `"node"` — grid cell reference, used without `fk_ref`; (3) 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". + +###### `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". + +#### Double + +Type `double`. + +##### Type-specific attributes + +###### `tagged` + +`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. + +###### `time_series` + +`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferrable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. + +#### Path + +Type `path`. + +##### Type-specific attributes + +###### `mode` + +`"filein" | "fileout"`. Whether the path is to an input or output file. + +### Composites + +Three kinds of composite type are relevant to MF6: [product](https://en.wikipedia.org/wiki/Product_type) (record), [sum](https://en.wikipedia.org/wiki/Tagged_union) (union), and collection (array, list). + +Composite fields are explicitly nested so that the composite structure is reflected in the schema. Product and sum types have multiple nested subfields. Lists have a single nested subfield. Arrays have no nested subfields; see below. + +#### Array + +Type `array`. + +Arrays are not proper composites. An array does not have an item subfield as does a list. Instead, it has a `dtype` attribute identifying its scalar element type. An array may not contain composite elements; `dtype` must be a scalar type. + +##### Type-specific attributes + +###### `dtype` + +`string`. The array's data type. Must be one of the scalar types. + +###### `shape` + +`[string]`. The array's shape. Each element is a shape expression — either a global dimension name (explicit or derived; see [Array dimensions](#array-dimensions)) or a row-level column lookup (see [Row-level column lookups](#row-level-column-lookups)). The latter form is only valid when the array is a subfield of a record. + +For `dtype: "string"` arrays, `shape` must be empty (`[]`). String arrays are read inline and self-sizing; no shape expression is needed or meaningful. Shape expressions on string arrays are validation errors. + +###### `time_series` + +`boolean (default: false)`. Marks fields where the READARRAY invocation may be replaced by a TAS name referencing a `utl-tas` time-array series object. At any model time, the TAS provides an interpolated grid-shaped array. Distinct from the scalar case: references `utl-tas`, not `utl-ts`. Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. + +###### `repeat` + +`string | null (default: null)`. Names the field (within the same component) whose runtime length determines how many times this field is read sequentially within an array block, with each reading appended to an accumulated sequence. See `repeat` section below. + +###### `dimension` + +`"component" | "model" | "simulation" | null (default: null)`. Valid only when `dtype` is `"string"`. Marks this array as a named dimension source at the given scope: the array's name may appear in other arrays' `shape` expressions to mean "one value per element of this string array." Because string arrays are always read inline (not via READARRAY), the MF6 parser counts tokens on the fly; no numeric value needs to be declared in advance. `shape` must be empty for any array with `dimension` set. Not meaningful on non-string arrays; `"record"` scope is not valid for arrays. + +#### Record + +Type `record`. Product type. In MF6 input files, records appear on a single line. Record subfields may or may not be `tagged`. While blocks can be considered product types also, in the DFN specification only records are considered fields; blocks are considered named collections of related fields. + +##### Type-specific attributes + +###### `fields` + +`{string: Scalar | Array | Record | Union}`. Subfields, required. + +**Note:** An array appearing as a subfield of a record is read inline on the same line, not in the READARRAY format. If the array's `shape` uses a row-level column lookup, the record is effectively a variadic tuple: its width varies per row as determined by a column in a FK-linked list. See [Row-level column lookups](#row-level-column-lookups). + +**Note:** if a nested record appears inside another record, the inner record's contents should appear inline inside the outer record's contents, on the same line. + +#### Union + +Type `union`. Sum type. + +##### Type-specific attributes + +###### `arms` + +`{string: Scalar | Record}`. Subfields, required. + +#### 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. + +##### Type-specific attributes + +###### `item` + +`Record | Union`. Subfield (item type), required. + +### Array dimensions + +A field defined in one component can be referenced by name in the `shape` expression of an array field in the same or another component. Two field types may serve as dimension sources: + +- `integer` fields with `dimension: true` +- `array` fields with `dtype: "string"` and `dimension: true` — the array's name becomes a valid shape dim meaning "one value per element of this string array" + +Shape expressions for non-string arrays may use one of three structural forms. All three may additionally carry a bound annotation: + +- **Dim reference** (`^[A-Za-z_]\w*$`): a plain identifier resolved via the scope chain (explicit → derived → inherited dims). When the array is a subfield of a record and the identifier does not resolve globally, resolution falls back to intra-record sibling scope (see below). +- **Intra-record sibling reference**: a dim reference that names a sibling `integer` or `dimension: true` `array` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. +- **Row-level column lookup** (`block.column(fk_field)`): a cross-list per-row quantity, valid only for array subfields of records. See below. + +Any dim reference (either of the first two forms) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`), e.g. ``, `<=`, `>=`) to express an advisory bound on the array's length, e.g. ``, e.g.: - -```shell -doctoc DEVELOPER.md -``` - -This will insert HTML comments surrounding an automatically edited region, in which `doctoc` will create an appropriately indented TOC tree. Subsequent runs are idempotent, scanning for headers and only updating the TOC if the file header structure has changed. - -To run `doctoc` for all markdown files in a particular directory (recursive), use `doctoc some/path`. - -By default `doctoc` inserts a self-descriptive comment - -> **Table of Contents** *generated with DocToc* - -This can be removed (and other content within the TOC region edited) — `doctoc` will not overwrite it, only the table. diff --git a/docs/md/programs.md b/docs/md/programs.md index d3942dbc..8d834927 100644 --- a/docs/md/programs.md +++ b/docs/md/programs.md @@ -1,16 +1,12 @@ # Programs API -> **Experimental API Warning** -> -> This API is experimental and may change or be removed in future versions without following normal deprecation procedures. Use at your own risk. +> **Warning**: This API is experimental and may change or be removed in future versions without following normal deprecation procedures. Use at your own risk. > > When importing this module programmatically, you will see a `FutureWarning`. To suppress this warning: > ```python > import warnings > warnings.filterwarnings('ignore', message='.*modflow_devtools.programs.*experimental.*') > ``` -> -> The `mf programs` CLI command is stable and does not trigger warnings. The `modflow_devtools.programs` module provides programmatic access to MODFLOW and related programs in the MODFLOW ecosystem. It can be used with MODFLOW organization releases or custom program repositories. diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index 156de0c6..98146a26 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -2,22 +2,27 @@ MODFLOW 6 definition file tools. """ +import re import warnings from abc import ABC, abstractmethod -from collections.abc import Iterator, Mapping -from dataclasses import asdict, dataclass, field, replace +from dataclasses import asdict from itertools import groupby from os import PathLike from pathlib import Path from typing import ( + Annotated, + Any, Literal, cast, ) +_IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") + import tomli from boltons.dictutils import OMD -from boltons.iterutils import remap from packaging.version import Version +from pydantic import BaseModel, ConfigDict, GetCoreSchemaHandler +from pydantic_core import core_schema from modflow_devtools.dfns.parse import ( is_advanced_package, @@ -27,12 +32,25 @@ try_parse_bool, try_parse_parent, ) -from modflow_devtools.dfns.schema.block import Block, Blocks, block_sort_key -from modflow_devtools.dfns.schema.field import Field, Fields from modflow_devtools.dfns.schema.v1 import SCALAR_TYPES as V1_SCALAR_TYPES from modflow_devtools.dfns.schema.v1 import FieldV1 -from modflow_devtools.dfns.schema.v2 import FieldV2 -from modflow_devtools.misc import drop_none_or_empty, try_literal_eval +from modflow_devtools.dfns.schema.v2 import ( + Array, + Block, + Blocks, + DfnSpec, + Double, + FieldBase, + FieldV2, + Integer, + Keyword, + List, + Path as PathField, + Record, + String, + Union, +) +from modflow_devtools.misc import try_literal_eval # Experimental API warning warnings.warn( @@ -46,6 +64,7 @@ ) __all__ = [ + "Array", "Block", "Blocks", "Dfn", @@ -55,13 +74,19 @@ "DfnRegistryNotFoundError", "DfnSpec", "Dfns", - "Field", + "Double", + "FieldBase", "FieldV1", "FieldV2", - "Fields", + "Integer", + "Keyword", + "List", "LocalDfnRegistry", + "PathField", + "Record", "RemoteDfnRegistry", - "block_sort_key", + "String", + "Union", "get_dfn", "get_dfn_path", "get_registry", @@ -85,8 +110,21 @@ Dfns = dict[str, "Dfn"] -@dataclass -class Dfn: +class _VersionAnnotation: + @classmethod + def __get_pydantic_core_schema__( + cls, source: Any, handler: GetCoreSchemaHandler + ) -> Any: + return core_schema.no_info_plain_validator_function( + lambda v: Version(str(v)) if not isinstance(v, Version) else v, + serialization=core_schema.to_string_ser_schema(), + ) + + +VersionField = Annotated[Version, _VersionAnnotation] + + +class Dfn(BaseModel): """ MODFLOW 6 input component definition. @@ -96,247 +134,118 @@ class Dfn: Schema version of this definition. name : str Component name (e.g., "gwf-chd", "sim-nam"). - parent : str | None - Parent component name (instance-level hierarchy). + parent : str | list[str] | None + Valid parent component type(s). advanced : bool Whether this is an advanced package. multi : bool Whether this is a multi-package. - ftype : str | None - File type identifier. - blocks : Blocks | None + variant_of : str | None + If set, names the canonical component this is a format variant of. + blocks : dict[str, Any] | None Block definitions containing field specifications. - children : Dfns | None - Actual child component instances (instance-level). + children : dict[str, Dfn] | None + Child component instances (populated by to_tree). subcomponents : list[str] | None Allowed child component types (schema-level constraint). - Populated from DFN comments like: # mf6 subpackage - Example: ['UTL-NCF'] means this component can have utl-ncf children. """ - schema_version: Version + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + + schema_version: VersionField name: str - parent: str | None = None + parent: str | list[str] | None = None advanced: bool = False multi: bool = False - ftype: str | None = None - blocks: Blocks | None = None - children: Dfns | None = None + variant_of: str | None = None + blocks: dict[str, Any] | None = None + children: "dict[str, Dfn] | None" = None subcomponents: list[str] | None = None @property - def fields(self) -> Fields: + def fields(self) -> Any: """ - A combined map of fields from all blocks. + Combined map of fields from all blocks (flat, top-level only). - Only top-level fields are included, no subfields of composites - such as records or recarrays. + Returns an OMD to support duplicate field names across v1 blocks. """ - fields = [] + items = [] for block in (self.blocks or {}).values(): for f in block.values(): - fields.append((f.name, f)) - - # for now return a multidict to support duplicate field names. - # TODO: change to normal dict after deprecating v1 schema - return OMD(fields) - - def __post_init__(self): - if not isinstance(self.schema_version, Version): - self.schema_version = Version(str(self.schema_version)) - if self.blocks: - self.blocks = dict(sorted(self.blocks.items(), key=block_sort_key)) + items.append((f.name, f)) + return OMD(items) @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "Dfn": """ - Create a Dfn instance from a dictionary. + Create a Dfn from a dictionary. Parameters ---------- d : dict - Dictionary containing DFN data + Dictionary containing DFN data. strict : bool, optional - If True, raise ValueError if dict contains unrecognized keys at the - top level or in nested field dicts. If False (default), ignore - unrecognized keys. + If True, raise ValueError for unrecognized keys at any level. """ - keys = list(cls.__annotations__.keys()) - if strict: - extra_keys = set(d.keys()) - set(keys) - if extra_keys: - raise ValueError(f"Unrecognized keys in DFN data: {extra_keys}") - data = {k: v for k, v in d.items() if k in keys} - schema_version = data.get("schema_version", Version("2")) - field_cls = FieldV1 if schema_version == Version("1") else FieldV2 - - def _fields(block_name, block_data): - fields = {} - for field_name, field_data in block_data.items(): - if isinstance(field_data, dict): - fields[field_name] = field_cls.from_dict(field_data, strict=strict) - elif isinstance(field_data, field_cls): - fields[field_name] = field_data - else: - raise TypeError( - f"Invalid field data for {field_name} in block {block_name}: " - f"expected dict or Field, got {type(field_data)}" - ) - return fields - - if blocks := data.get("blocks"): - data["schema_version"] = schema_version - data["blocks"] = { - block_name: _fields(block_name, block_data) - for block_name, block_data in blocks.items() - } - - return cls(**data) - - -@dataclass -class DfnSpec(Mapping): - """ - Full MODFLOW 6 input specification with hierarchical structure and flat dict access. - - The specification maintains a single canonical hierarchical representation via - the `root` property (simulation component with nested children), while also - providing flat dict-like access to any component by name via the Mapping protocol. + known_keys = set(cls.model_fields.keys()) + schema_version = Version(str(d.get("schema_version", "2"))) + is_v1 = schema_version == Version("1") - Parameters - ---------- - schema_version : Version - The schema version of the specification (e.g., "1", "1.1", "2"). - root : Dfn - The root component (simulation) with hierarchical children populated. - - Examples - -------- - >>> spec = DfnSpec.load("/path/to/dfns") - >>> spec.schema_version - Version('2') - >>> spec.root.name - 'sim-nam' - >>> spec["gwf-chd"] # Flat access by component name - Dfn(name='gwf-chd', ...) - >>> list(spec.keys())[:3] - ['sim-nam', 'sim-tdis', 'gwf-nam'] - """ - - schema_version: Version - root: "Dfn" - _flat: Dfns = field(default_factory=dict, repr=False, compare=False) - - def __post_init__(self): - if not isinstance(self.schema_version, Version): - self.schema_version = Version(str(self.schema_version)) - # Build flat index if not already populated - if not self._flat: - self._flat = to_flat(self.root) - - def __getitem__(self, name: str) -> "Dfn": - """Get a component by name (flattened lookup).""" - if name not in self._flat: - raise KeyError(f"Component '{name}' not found in specification") - return self._flat[name] - - def __iter__(self) -> Iterator[str]: - """Iterate over all component names.""" - return iter(self._flat) - - def __len__(self) -> int: - """Total number of components in the specification.""" - return len(self._flat) - - def __contains__(self, name: object) -> bool: - """Check if a component exists by name.""" - return name in self._flat - - def dump(self, f) -> None: - """Serialize the full spec to a TOML byte stream.""" - import tomli_w - - doc = {"schema_version": str(self.schema_version)} - for name, dfn in self._flat.items(): - doc[name] = _toml_safe(remap(asdict(dfn), visit=drop_none_or_empty)) - f.write(tomli_w.dumps(doc).encode()) - - def dumps(self) -> str: - """Serialize the full spec to a TOML string.""" - import io - - buf = io.BytesIO() - self.dump(buf) - return buf.getvalue().decode() - - @classmethod - def load( - cls, - path: str | PathLike, - schema_version: str | Version | None = None, - ) -> "DfnSpec": - """ - Load a specification from a directory of DFN files. + if strict: + extra = set(d.keys()) - known_keys + if extra: + raise ValueError(f"Unrecognized keys in DFN data: {extra}") + + data = {k: v for k, v in d.items() if k in known_keys} + data.setdefault("schema_version", schema_version) + + if blocks_raw := data.get("blocks"): + parsed_blocks: dict[str, Any] = {} + for block_name, block_data in blocks_raw.items(): + if not isinstance(block_data, dict): + parsed_blocks[block_name] = block_data + continue + block_fields: dict[str, Any] = {} + for field_name, field_data in block_data.items(): + if isinstance(field_data, dict): + if is_v1: + block_fields[field_name] = FieldV1.from_dict( + field_data, strict=strict + ) + else: + block_fields[field_name] = FieldBase.from_dict( + field_data, strict=strict + ) + else: + block_fields[field_name] = field_data + parsed_blocks[block_name] = block_fields + data["blocks"] = parsed_blocks - The specification is always loaded as a hierarchical tree, - with flat access available via the Mapping protocol. + return cls.model_validate(data) - Parameters - ---------- - path : str or PathLike - Path to directory containing DFN files. - schema_version : str or Version, optional - Target schema version. If provided and different from the native - schema version, DFNs will be mapped to the target version. - If not provided, uses the native schema version from the files. - - Returns - ------- - DfnSpec - The loaded specification with hierarchical structure. - - Examples - -------- - >>> spec = DfnSpec.load("/path/to/dfns") - >>> spec.root.name - 'sim-nam' - >>> spec["gwf-dis"] - Dfn(name='gwf-dis', ...) - """ - path = Path(path).expanduser().resolve() - - # Load flat DFNs from directory - dfns = load_flat(path) - - if not dfns: - raise ValueError(f"No DFN files found in {path}") - - # Determine native schema version from first DFN - first_dfn = next(iter(dfns.values())) - native_version = first_dfn.schema_version - - # Determine target version: - # - If explicitly specified, use that - # - If native is v1, default to v2 (since to_tree only works with v2) - # - Otherwise use native version - if schema_version: - target_version = Version(str(schema_version)) - elif native_version == Version("1"): - target_version = Version("2") - else: - target_version = native_version - if target_version != native_version: - # Map DFNs to target schema version - dfns = {name: map(dfn, target_version) for name, dfn in dfns.items()} +def _dfn_to_plain_dict(dfn: "Dfn") -> dict: + """Serialize a Dfn to a plain Python dict, serializing any Pydantic field instances.""" + d = dfn.model_dump(exclude_none=True) + if blocks := d.get("blocks"): + for block_name, block_fields in blocks.items(): + for field_name, field_val in list(block_fields.items()): + if isinstance(field_val, BaseModel): + blocks[block_name][field_name] = field_val.model_dump(exclude_none=True) + return d - # Build hierarchical tree - root = to_tree(dfns) - return cls( - schema_version=target_version, - root=root, - ) +def _toml_safe(obj: Any) -> Any: + """Recursively coerce non-TOML-native types to str.""" + if isinstance(obj, BaseModel): + return _toml_safe(obj.model_dump(exclude_none=True)) + if isinstance(obj, dict): + return {k: _toml_safe(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_toml_safe(v) for v in obj] + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + return str(obj) class SchemaMap(ABC): @@ -346,251 +255,641 @@ def map(self, dfn: Dfn) -> Dfn: ... class MapV1To2(SchemaMap): @staticmethod - def map_period_block(dfn: Dfn, block: Block) -> Block: + def map_period_block(dfn: "Dfn", block: dict) -> dict: """ Convert a period block recarray to individual arrays, one per column. - - Extracts recarray fields and creates separate array variables. Gives - each an appropriate grid- or tdis-aligned shape as opposed to sparse - list shape in terms of maxbound as previously. """ - block = dict(block) - fields = list(block.values()) - if fields[0].type == "list": - assert len(fields) == 1 - recarray_name = fields[0].name - block.pop(recarray_name, None) - item = next(iter((fields[0].children or {}).values())) - columns = dict(item.children or {}) + fields_list = list(block.values()) + + if fields_list and isinstance(fields_list[0], List): + assert len(fields_list) == 1 + list_field: List = fields_list[0] + block.pop(list_field.name) + item = list_field.item + columns: dict = dict( + item.fields if isinstance(item, Record) else item.arms + ) else: - recarray_name = None - columns = block + columns = dict(block) cellid = columns.pop("cellid", None) + + _SCALAR_DTYPES = {"keyword", "integer", "double", "double precision", "string"} + for col_name, column in columns.items(): - old_dims = column.shape - if old_dims: - old_dims = old_dims[1:-1].split(",") # type: ignore + if isinstance(column, Array): + dtype = column.dtype + elif getattr(column, "type", None) in _SCALAR_DTYPES: + dtype = column.type + if dtype == "double precision": + dtype = "double" + else: + # Composite column (Record, Union, etc.) — keep as-is + block[col_name] = column + continue + + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + old_dims = list(column.shape) if isinstance(column, Array) else [] new_dims = ["nper"] if cellid: - new_dims.append("nnodes") - if old_dims: - new_dims.extend([dim for dim in old_dims if dim != "maxbound"]) - block[col_name] = replace(column, shape=f"({', '.join(new_dims)})") + new_dims.append("nodes") + # Only carry structural grid dims forward; runtime-only inline + # counts (e.g. naux) are not declared shape dims in v2. + new_dims.extend(d for d in old_dims if d in GRID_DIM_NAMESPACE) + + block[col_name] = Array( + name=column.name, + longname=getattr(column, "longname", None), + description=getattr(column, "description", None), + optional=column.optional, + default=getattr(column, "default", None), + developmode=column.developmode, + netcdf=getattr(column, "netcdf", False), + dtype=dtype, + shape=new_dims, + ) return block @staticmethod - def map_field(dfn: Dfn, field: Field) -> Field: + def map_field(dfn: "Dfn", v1_field: FieldV1) -> FieldBase: """ - Convert an input field specification from its representation - in a v1 format definition file to the v2 (structured) format. + Convert a v1 field to the appropriate v2 concrete type. + """ + fields = cast(OMD, dfn.fields) - Notes - ----- - If the field does not have a `default` attribute, it will - default to `False` if it is a keyword, otherwise to `None`. + def _to_bool(v: Any, default: bool = False) -> bool: + if isinstance(v, bool): + return v + if isinstance(v, str): + s = v.strip().lower() + if s == "true": + return True + if s in ("false", ""): + return False + return default + + def _map_field(f: FieldV1) -> FieldBase: + fd = asdict(f) + fd = {k: try_parse_bool(v) for k, v in fd.items()} + + _name: str = fd["name"] + _type: str | None = fd.get("type") + shape_str: str | None = fd.get("shape") or None + description: str | None = fd.get("description") or None + longname: str | None = fd.get("longname") or None + optional: bool = _to_bool(fd.get("optional"), False) + developmode: bool = _to_bool(fd.get("developmode"), False) + netcdf: bool = _to_bool(fd.get("netcdf"), False) + tagged: bool = _to_bool(fd.get("tagged"), False) + preserve_case: bool = _to_bool(fd.get("preserve_case"), False) + time_series: bool = _to_bool(fd.get("time_series"), False) + valid = fd.get("valid") + default = ( + try_literal_eval(fd.get("default")) + if _type != "string" + else fd.get("default") + ) - A filepath field whose name functions as a foreign key - for a separate context will be given a reference to it. - """ + common = dict( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + ) - fields = cast(OMD, dfn.fields) + _COL_FK_RE = re.compile(r"^([A-Za-z_]\w*)\(([A-Za-z_]\w*)\)$") + + def _parse_shape(s: str) -> list[str]: + result = [] + s_clean = s.strip() + # Strip exactly one pair of outer parentheses to avoid munging + # nested forms like (ncon(ifno)). + if s_clean.startswith("(") and s_clean.endswith(")"): + s_clean = s_clean[1:-1] + for elem in (x.strip() for x in s_clean.split(",") if x.strip()): + if ";" in elem: + # v1 discretization-conditional (e.g. "ncol*nrow; ncpl") + # → canonical per-layer count; DIS derives ncpl = nrow*ncol. + result.append("ncpl") + elif elem in ("any1d", "unknown") or elem.startswith("<") or elem.startswith(">"): + # v1 pseudo-elements with no v2 shape equivalent: + # any1d — inline array of runtime-determined length + # (read to end of record); dtype-agnostic. + # X — bound annotations (e.g. " once v1 DFN typos are fixed. + pass + elif m := _COL_FK_RE.fullmatch(elem): + # v1 shorthand: column(fk_field) with no block prefix. + # Resolve the block by searching for the integer field. + col_name = m.group(1) + block_name = next( + (fi.block for fi in fields.values(multi=True) + if fi.name == col_name + and fi.type == "integer" + and fi.in_record), + None, + ) + if block_name: + result.append(f"{block_name}.{elem}") + # else: unresolvable; drop with no shape element + else: + # Check if elem is an implicit count (e.g. "naux") for a + # string array whose v1 shape is (elem). If so, emit the + # string array's name so _mark_string_dim_arrays can mark + # it dimension="component" and validation resolves it. + provider = next( + (fi.name for fi in fields.values(multi=True) + if fi.type == "string" + and (fi.shape or "").strip() in (f"({elem})", elem)), + None, + ) + result.append(provider if provider else elem) + return result + + def _to_scalar() -> FieldBase: + assert _type is not None + if _type == "keyword": + return Keyword(**common, netcdf=netcdf) + if _type == "string": + return String( + **common, + netcdf=netcdf, + tagged=tagged, + valid=list(valid) if valid else None, + case_sensitive=preserve_case, + time_series=time_series, + ) + if _type == "integer": + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + v = [int(x) for x in valid] if valid else None + if fd.get("block") == "dimensions": + if _name in GRID_DIM_NAMESPACE: + _dim_scope: str | None = "model" + elif dfn.name == "sim-tdis" and _name == "nper": + _dim_scope = "simulation" + else: + _dim_scope = "component" + else: + _dim_scope = None + return Integer( + **common, + netcdf=netcdf, + tagged=tagged, + valid=v, + time_series=time_series, + dimension=_dim_scope, + ) + if _type in ("double", "double precision"): + return Double( + **common, netcdf=netcdf, tagged=tagged, time_series=time_series + ) + raise TypeError(f"Unsupported scalar type: {_type!r}") + + def _row_field() -> "Record | Union": + item_names = (_type or "").split()[1:] + if not item_names: + raise ValueError(f"Missing list item definition: {_type!r}") - def _map_field(_field) -> Field: - field_dict = asdict(_field) - # parse booleans from strings. everything else can - # stay a string except default values, which we'll - # try to parse as arbitrary literals below, and at - # some point types, once we introduce type hinting - field_dict = {k: try_parse_bool(v) for k, v in field_dict.items()} - _name = field_dict.pop("name") - _type = field_dict.pop("type", None) - shape = field_dict.pop("shape", None) - shape = None if shape == "" else shape - block = field_dict.pop("block", None) - default = field_dict.pop("default_value", None) - default = try_literal_eval(default) if _type != "string" else default - description = field_dict.pop("description", "") - - def _row_field() -> Field: - """Parse a table's record (row) field""" - item_names = _type.split()[1:] item_types = [ - f.type - for f in fields.values(multi=True) - if f.name in item_names and f.in_record + fi.type + for fi in fields.values(multi=True) + if fi.name in item_names and fi.in_record ] - n_item_names = len(item_names) - if n_item_names < 1: - raise ValueError(f"Missing list definition: {_type}") - # explicit record or keystring - if n_item_names == 1 and ( - item_types[0].startswith("record") or item_types[0].startswith("keystring") + # Single explicit record or keystring + if len(item_names) == 1 and item_types and ( + (item_types[0] or "").startswith("record") + or (item_types[0] or "").startswith("keystring") ): - return MapV1To2.map_field(dfn, next(iter(fields.getlist(item_names[0])))) + mapped = MapV1To2.map_field( + dfn, next(iter(fields.getlist(item_names[0]))) + ) + if isinstance(mapped, (Record, Union)): + return mapped + raise TypeError( + f"Expected Record or Union for list item, got {type(mapped).__name__}" + ) - # implicit record with all scalar fields + # All scalars → implicit Record if all(t in V1_SCALAR_TYPES for t in item_types): - children = _record_fields() - return FieldV2.from_dict( - { - **field_dict, - "name": _name, - "type": "record", - "block": block, - "children": children, - "description": description.replace( - "is the list of", "is the record of" - ), - } + rec_fields = _record_fields() + return Record( + name=_name, + description=( + (description or "").replace("is the list of", "is the record of") + or None + ), + fields=rec_fields, ) - # implicit record with composite fields + # Mixed composites children = { - f.name: MapV1To2.map_field(dfn, f) - for f in fields.values(multi=True) - if f.name in item_names and f.in_record + fi.name: MapV1To2.map_field(dfn, fi) + for fi in fields.values(multi=True) + if fi.name in item_names and fi.in_record } first = next(iter(children.values())) - if not first.type: - raise ValueError(f"Missing type for field: {first.name}") - single = len(children) == 1 - item_type = "keystring" if single and "keystring" in first.type else "record" - return FieldV2.from_dict( - { - "name": first.name if single else _name, - "type": item_type, - "block": block, - "children": first.children if single else children, - "description": description.replace( - "is the list of", f"is the {item_type} of" - ), - **field_dict, - } + if len(children) == 1 and isinstance(first, Union): + return first + return Record( + name=_name, + description=( + (description or "").replace("is the list of", "is the record of") + or None + ), + fields=children, ) - def _union_fields() -> Fields: - """Parse a union's fields""" - names = _type.split()[1:] + def _union_fields() -> dict: + names = (_type or "").split()[1:] return { - f.name: MapV1To2.map_field(dfn, f) - for f in fields.values(multi=True) - if f.name in names and f.in_record + fi.name: MapV1To2.map_field(dfn, fi) + for fi in fields.values(multi=True) + if fi.name in names and fi.in_record } - def _record_fields() -> Fields: - """Parse a record's fields""" - names = _type.split()[1:] + def _record_fields() -> dict: + names = (_type or "").split()[1:] result = {} - for name in names: - matching = [ - f - for f in fields.values(multi=True) - if f.name == name and f.in_record and not f.type.startswith("record") + for rname in names: + matches = [ + fi + for fi in fields.values(multi=True) + if fi.name == rname + and fi.in_record + and not (fi.type or "").startswith("record") ] - if matching: - result[name] = _map_field(matching[0]) + if matches: + result[rname] = _map_field(matches[0]) return result - _field = FieldV2.from_dict( - { - "name": _name, - "shape": shape, - "block": block, - "description": description, - "default": default, - **field_dict, - } - ) + if _type is None: + raise ValueError(f"Missing type for v1 field: {_name!r}") if _type.startswith("recarray"): - child = _row_field() - _field.children = {child.name: child} - _field.type = "list" + item = _row_field() + return List(item=item, **common, netcdf=netcdf) - elif _type.startswith("keystring"): - _field.children = _union_fields() - _field.type = "union" + if _type.startswith("keystring"): + arms = _union_fields() + return Union(arms=arms, **common) - elif _type.startswith("record"): - _field.children = _record_fields() - _field.type = "record" + if _type.startswith("record"): + rec_fields = _record_fields() + return Record(fields=rec_fields, **common) - # for now, we can tell a var is an array if its type - # is scalar and it has a shape. once we have proper - # typing, this can be read off the type itself. - elif shape is not None and _type not in V1_SCALAR_TYPES: - raise TypeError(f"Unsupported array type: {_type}") - - else: - # Map v1 type names to v2 type names - type_map = { + if shape_str is not None: + dtype_map = { "double precision": "double", + "double": "double", + "integer": "integer", + "string": "string", + "keyword": "keyword", } - _field.type = type_map.get(_type, _type) + dtype = dtype_map.get(_type) + if dtype is not None: + if dtype == "string": + # String arrays are inline and self-sizing in v2; drop + # the v1 shape expression. dimension=True is set in a + # second pass if another array references this field. + return Array( + **common, + netcdf=netcdf, + time_series=time_series, + dtype="string", + shape=[], + ) + parsed_shape = _parse_shape(shape_str) + return Array( + **common, + netcdf=netcdf, + time_series=time_series, + dtype=dtype, + shape=parsed_shape, + ) - return _field + return _to_scalar() - return _map_field(field) + return _map_field(v1_field) @staticmethod - def map_blocks(dfn: Dfn) -> Blocks: - fields = { - field.name: MapV1To2.map_field(dfn, field) - for field in cast(OMD, dfn.fields).values(multi=True) - if not field.in_record # type: ignore - } - block_dicts = { - block_name: {f.name: f for f in block} - for block_name, block in groupby(fields.values(), lambda f: f.block) - } - blocks = {} + def _mark_dimension_fields(blocks: "dict[str, dict]") -> "dict[str, dict]": + """ + Post-pass: annotate every field that provides a dimension count. - # Handle period blocks specially - if (period_block := block_dicts.get("period", None)) is not None: - blocks["period"] = MapV1To2.map_period_block(dfn, period_block) + Two concerns are handled in one pass because they share a scan phase + and have an ordering dependency — string-array dim providers must be + known before record-local dim integers can be identified (so their + names are excluded from the "local" check). - for block_name, block_data in block_dicts.items(): - if block_name != "period": - blocks[block_name] = block_data + String-array dim providers (e.g. ``auxiliary``): a string Array whose + name is referenced as a plain identifier in any non-string Array's + shape. Marked ``dimension="component"``. - def remove_attrs(path, key, value): - # remove unneeded variable attributes - if key in ["in_record", "tagged", "preserve_case"]: - return False - return True + Record-local dim integers: an Integer field inside a Record that is + named by a sibling non-string Array's shape element and does not + resolve to any globally-scoped dim. Marked ``dimension="record"``. + """ + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + + # ── Phase 1: single scan ────────────────────────────────────────── + # Collect plain-identifier shape refs from non-string Arrays, names + # of all string Arrays, and any already-explicit global dim names. + shape_refs: set[str] = set() + string_array_names: set[str] = set() + explicit_globals: set[str] = set(GRID_DIM_NAMESPACE) + + def _scan(fields: dict) -> None: + for f in fields.values(): + if isinstance(f, Array): + if f.dtype != "string": + for elem in f.shape: + if _IDENT_RE.fullmatch(elem): + shape_refs.add(elem) + else: + string_array_names.add(f.name) + scope = getattr(f, "dimension", None) + if scope in ("component", "model", "simulation"): + explicit_globals.add(f.name) + if isinstance(f, Record): + _scan(f.fields) + elif isinstance(f, Union): + _scan(f.arms) + elif isinstance(f, List): + item = f.item + _scan(item.fields if isinstance(item, Record) else item.arms) + + for block_fields in blocks.values(): + _scan(block_fields) + + if not shape_refs: + return blocks + + # ── Phase 2: derive complete global dim set ─────────────────────── + # String arrays referenced by name in a non-string array's shape are + # dim providers; add them to global_dims so they are not mistakenly + # identified as record-local dims in the mark phase below. + string_provider_names: set[str] = string_array_names & shape_refs + global_dims: set[str] = explicit_globals | string_provider_names + + # ── Phase 3: mark ───────────────────────────────────────────────── + def _record_local_dims(rec: "Record") -> set[str]: + """Integer field names in rec that serve as per-row inline counts.""" + to_mark: set[str] = set() + for sf in rec.fields.values(): + if isinstance(sf, Array) and sf.dtype != "string": + for elem in sf.shape: + if _IDENT_RE.fullmatch(elem) and elem not in global_dims: + sibling = rec.fields.get(elem) + if isinstance(sibling, Integer) and sibling.dimension is None: + to_mark.add(elem) + return to_mark + + def _mark(fields: dict) -> dict: + result = {} + for name, f in fields.items(): + if isinstance(f, Array) and f.dtype == "string" and name in string_provider_names: + f = f.model_copy(update={"dimension": "component"}) + elif isinstance(f, Record): + local_dims = _record_local_dims(f) + new_fields = _mark(f.fields) + if local_dims: + new_fields = { + fn: (sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf) + for fn, sf in new_fields.items() + } + f = f.model_copy(update={"fields": new_fields}) + elif isinstance(f, Union): + f = f.model_copy(update={"arms": _mark(f.arms)}) + elif isinstance(f, List): + item = f.item + if isinstance(item, Record): + local_dims = _record_local_dims(item) + new_item_fields = _mark(item.fields) + if local_dims: + new_item_fields = { + fn: (sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf) + for fn, sf in new_item_fields.items() + } + new_item = item.model_copy(update={"fields": new_item_fields}) + elif isinstance(item, Union): + new_item = item.model_copy(update={"arms": _mark(item.arms)}) + else: + new_item = item + f = f.model_copy(update={"item": new_item}) + result[name] = f + return result - return remap(blocks, visit=remove_attrs) + return {bn: _mark(bf) for bn, bf in blocks.items()} - def map(self, dfn: Dfn) -> Dfn: - if dfn.schema_version == (v2 := Version("2")): - return dfn + @staticmethod + def _infer_fk_from_shapes(blocks: "dict[str, dict]") -> "dict[str, dict]": + """ + Fourth pass: infer fk= and pk= from resolved lookup shape elements. + + When _parse_shape resolves a v1 shorthand like "col(fk_field)" to the + canonical form "block.col(fk_field)", the fk_field sibling in the same + enclosing record implicitly references that block. This pass: + - infers fk = "block.fk_field" on that sibling (enables check 4 of + _validate_shape_element), and + - infers pk = True on the same-named field in the referenced block's + list item (required by _validate_fk_fields). + Both marks are only applied when the attribute is not already set. + """ + _lookup_re = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") + + # Phase 1: collect inferred FK and PK pairs. + # fk_map: (enclosing_block, fk_field_name) -> fk value string + # pk_set: (pk_block, pk_field_name) pairs that need pk=True + fk_map: dict[tuple[str, str], str] = {} + pk_set: set[tuple[str, str]] = set() + + def _scan_record(rec: "Record", block_name: str) -> None: + for sf in rec.fields.values(): + if isinstance(sf, Array): + for elem in sf.shape: + m = _lookup_re.fullmatch(elem) + if m: + pk_block, _col, fk_fname = m.groups() + sibling = rec.fields.get(fk_fname) + if sibling is not None and getattr(sibling, "fk", None) is None: + fk_map[(block_name, fk_fname)] = f"{pk_block}.{fk_fname}" + pk_set.add((pk_block, fk_fname)) + + def _scan(fields: dict, block_name: str) -> None: + for f in fields.values(): + if isinstance(f, Record): + _scan_record(f, block_name) + elif isinstance(f, Union): + _scan(f.arms, block_name) + elif isinstance(f, List): + item = f.item + if isinstance(item, Record): + _scan_record(item, block_name) + elif isinstance(item, Union): + _scan(item.arms, block_name) + + for block_name, block_fields in blocks.items(): + _scan(block_fields, block_name) + + if not fk_map and not pk_set: + return blocks + + # Phase 2: apply FK and PK markings. + def _apply_record(rec: "Record", block_name: str) -> "Record": + updates: dict = {} + for fname, sf in rec.fields.items(): + updated = sf + if (block_name, fname) in fk_map and getattr(sf, "fk", None) is None: + updated = updated.model_copy(update={"fk": fk_map[(block_name, fname)]}) + if (block_name, fname) in pk_set and not getattr(sf, "pk", False): + updated = updated.model_copy(update={"pk": True}) + if updated is not sf: + updates[fname] = updated + if not updates: + return rec + return rec.model_copy( + update={"fields": {fn: updates.get(fn, sf) for fn, sf in rec.fields.items()}} + ) + + def _apply(fields: dict, block_name: str) -> dict: + result = {} + for name, f in fields.items(): + if isinstance(f, Record): + f = _apply_record(f, block_name) + elif isinstance(f, Union): + f = f.model_copy(update={"arms": _apply(f.arms, block_name)}) + elif isinstance(f, List): + item = f.item + if isinstance(item, Record): + new_item = _apply_record(item, block_name) + elif isinstance(item, Union): + new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) + else: + new_item = item + f = f.model_copy(update={"item": new_item}) + result[name] = f + return result + return {bn: _apply(bf, bn) for bn, bf in blocks.items()} + + @staticmethod + def map_blocks(dfn: "Dfn") -> dict: + """ + Convert all v1 fields in a DFN to v2 types and return a block dict. + + Structured as three phases; phases 2 and 3 are post-passes because + cross-field relationships cannot be determined field-by-field: + + 1. Field conversion (``map_field`` per top-level field): translate + each FieldV1 to the appropriate v2 concrete type. Per-field only; + cross-field relationships are deferred. + + 2. Dimension annotation (``_mark_dimension_fields``): scan the + completed blocks and annotate every dimension-providing field: + string Arrays referenced by non-string Array shapes (→ + ``dimension="component"``) and Record-local inline-count Integers + (→ ``dimension="record"``). + + 3. FK/PK inference (``_infer_fk_from_shapes``): scan for resolved + ``block.col(fk_field)`` shape elements and infer the corresponding + ``fk=`` and ``pk=`` annotations. Only applies to components with + implicit cross-block FK relationships (gwf-sfr in v1). + """ + all_v1 = cast(OMD, dfn.fields) + grouped: dict[str, dict] = {} + for v1_field in all_v1.values(multi=True): + if v1_field.in_record: # type: ignore[attr-defined] + continue + block_name = v1_field.block + mapped = MapV1To2.map_field(dfn, v1_field) + grouped.setdefault(block_name, {})[v1_field.name] = mapped + + blocks: dict[str, dict] = {} + if period := grouped.pop("period", None): + blocks["period"] = MapV1To2.map_period_block(dfn, period) + for block_name, block_data in grouped.items(): + blocks[block_name] = block_data + + blocks = MapV1To2._mark_dimension_fields(blocks) + return MapV1To2._infer_fk_from_shapes(blocks) + + @staticmethod + def to_component(dfn: "Dfn") -> "Any": + """ + Convert a v2 Dfn to the appropriate Component + (Simulation, Model, or Package), inferring the type from the + component name and parsed metadata. + + The Dfn must already be at schema version 2 (field types must be + v2 concrete FieldBase instances, not FieldV1 dataclasses). + """ + from modflow_devtools.dfns.schema.v2 import ( + Block, + Model, + Package, + Simulation, + ) + + name = dfn.name + blocks: "dict[str, Block] | None" = None + if dfn.blocks: + blocks = { + block_name: Block( + name=block_name, + fields={ + k: v + for k, v in block_fields.items() + if isinstance(v, FieldBase) + }, + ) + for block_name, block_fields in dfn.blocks.items() + if isinstance(block_fields, dict) + } + + common: dict[str, Any] = dict( + name=name, + blocks=blocks, + parent=dfn.parent, + schema_version=dfn.schema_version, + ) + if name == "sim-nam": + return Simulation(**common) + if name.endswith("-nam"): + return Model(**common) + if name.startswith("sln-"): + return Package(**common, subtype="solution", multi=dfn.multi, variant_of=dfn.variant_of) + if name.startswith("exg-"): + return Package(**common, subtype="exchange", multi=dfn.multi, variant_of=dfn.variant_of) + if name.startswith("utl-"): + return Package(**common, subtype="utility", multi=dfn.multi, variant_of=dfn.variant_of) + has_period = bool(blocks and any("period" in k for k in blocks)) + subtype = "advanced" if dfn.advanced else "stress" if has_period else None + return Package(**common, subtype=subtype, multi=dfn.multi, variant_of=dfn.variant_of) + + def map(self, dfn: "Dfn") -> "Dfn": + if dfn.schema_version == Version("2"): + return dfn return Dfn( name=dfn.name, + schema_version=Version("2"), + parent=dfn.parent, advanced=dfn.advanced, multi=dfn.multi, - ftype=dfn.ftype or (dfn.name.split("-", 1)[1].upper() if "-" in dfn.name else None), + variant_of=dfn.variant_of, blocks=MapV1To2.map_blocks(dfn), - schema_version=v2, - parent=dfn.parent, + subcomponents=dfn.subcomponents, ) -def _toml_safe(obj): - """Recursively coerce non-TOML-native types to str.""" - if isinstance(obj, dict): - return {k: _toml_safe(v) for k, v in obj.items()} - if isinstance(obj, list): - return [_toml_safe(v) for v in obj] - if isinstance(obj, (str, int, float, bool)) or obj is None: - return obj - return str(obj) - - def map( dfn: Dfn, schema_version: str | Version = "2", @@ -610,11 +909,14 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn: """Load a MODFLOW 6 definition file.""" if format == "dfn": name = kwargs.pop("name") - fields, meta = parse_dfn(f, **kwargs) + fields_parsed, meta = parse_dfn(f, **kwargs) blocks = { - block_name: {field["name"]: FieldV1.from_dict(field) for field in block} + block_name: { + field_dict["name"]: FieldV1.from_dict(field_dict) + for field_dict in block + } for block_name, block in groupby( - fields.values(multi=True), lambda field: field["block"] + fields_parsed.values(multi=True), lambda fd: fd["block"] ) } subcomponents = parse_mf6_subpackages(meta) @@ -624,7 +926,6 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn: parent=try_parse_parent(meta), advanced=is_advanced_package(meta), multi=is_multi_package(meta), - ftype=name.split("-", 1)[1].upper() if "-" in name else None, blocks=blocks, subcomponents=subcomponents if subcomponents else None, ) @@ -633,39 +934,39 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn: data = tomli.load(f) dfn_name = data.pop("name", kwargs.pop("name", None)) - dfn_fields = { + dfn_fields: dict[str, Any] = { "name": dfn_name, "schema_version": Version(str(data.pop("schema_version", "2"))), "parent": data.pop("parent", None), "advanced": data.pop("advanced", False), "multi": data.pop("multi", False), - "ftype": data.pop("ftype", None) - or (dfn_name.split("-", 1)[1].upper() if dfn_name and "-" in dfn_name else None), + "variant_of": data.pop("variant_of", None), } if (expected_name := kwargs.pop("name", None)) is not None: if dfn_fields["name"] != expected_name: - raise ValueError(f"DFN name mismatch: {expected_name} != {dfn_fields['name']}") + raise ValueError( + f"DFN name mismatch: {expected_name} != {dfn_fields['name']}" + ) blocks = {} for section_name, section_data in data.items(): if isinstance(section_data, dict): - block_fields = {} + block_fields: dict[str, Any] = {} for field_name, field_data in section_data.items(): if isinstance(field_data, dict): - block_fields[field_name] = FieldV2.from_dict(field_data) + block_fields[field_name] = FieldBase.from_dict(field_data) else: block_fields[field_name] = field_data - blocks[section_name] = block_fields # type: ignore + blocks[section_name] = block_fields dfn_fields["blocks"] = blocks if blocks else None - return Dfn(**dfn_fields) raise ValueError(f"Unsupported format: {format}. Expected 'dfn' or 'toml'.") -def _load_common(f) -> Fields: +def _load_common(f) -> Any: common, _ = parse_dfn(f) return common @@ -675,10 +976,10 @@ def load_flat(path: str | PathLike) -> Dfns: Load a flat MODFLOW 6 specification from definition files in a directory. Returns a dictionary of unlinked DFNs, i.e. without `children` populated. - Components will have `parent` populated if the schema is v2 but not if v1. """ exclude = ["common", "flopy"] path = Path(path).expanduser().resolve() + dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.stem not in exclude} toml_paths = {p.stem: p for p in path.glob("*.toml") if p.stem not in exclude} dfns = {} @@ -699,81 +1000,88 @@ def load_tree(path: str | PathLike) -> Dfn: """ Load a structured MODFLOW 6 specification from definition files in a directory. - A single root component definition (the simulation) is returned. This contains - child (and grandchild) components for the relevant models and packages. + A single root component definition (the simulation) is returned with + nested children. """ return to_tree(load_flat(path)) -def to_tree(dfns: Dfns) -> Dfn: - """ - Infer the MODFLOW 6 input component hierarchy from a flat spec: - unlinked DFNs, i.e. without `children` populated, only `parent`. +def _infer_parent(name: str) -> "str | None": + """Infer the parent component name from a component name using MF6 conventions.""" + if name == "sim-nam": + return None + if name.endswith("-nam"): + return "sim-nam" + if name.startswith(("exg-", "sln-", "utl-")): + return "sim-nam" + if "-" in name: + mdl = name.split("-")[0] + return f"{mdl}-nam" + return None + + +def _apply_parent_inference(dfns: Dfns) -> Dfns: + """Set parent on any Dfn where it is not already explicit.""" + result = {} + for name, dfn in dfns.items(): + if dfn.parent is None: + inferred = _infer_parent(name) + result[name] = dfn.model_copy(update={"parent": inferred}) if inferred else dfn + else: + result[name] = dfn + return result - Returns the root component. There must be exactly one root, i.e. - component with no `parent`. Composite components have `children` - populated. - Assumes DFNs are already in v2 schema, just lacking parent-child - links; before calling this function, map them first with `map()`. +def to_tree(dfns: Dfns) -> Dfn: """ + Infer the MODFLOW 6 input component hierarchy from a flat spec. - def set_parent(dfn): - dfn = asdict(dfn) - if (dfn_name := dfn["name"]) == "sim-nam": - pass - elif dfn_name.endswith("-nam"): - dfn["parent"] = "sim-nam" - elif ( - dfn_name.startswith("exg-") - or dfn_name.startswith("sln-") - or dfn_name.startswith("utl-") - ): - dfn["parent"] = "sim-nam" - elif "-" in dfn_name: - mdl = dfn_name.split("-")[0] - dfn["parent"] = f"{mdl}-nam" - - return Dfn(**remap(dfn, visit=drop_none_or_empty)) - - dfns = {name: set_parent(dfn) for name, dfn in dfns.items()} + Returns the root component. There must be exactly one root (no parent). + Assumes DFNs are already in v2 schema. + """ + dfns = _apply_parent_inference(dfns) first_dfn = next(iter(dfns.values()), None) + match schema_version := str(first_dfn.schema_version if first_dfn else Version("1")): case "1": raise NotImplementedError("Tree inference from v1 schema not implemented") case "2": - if ( - nroots := len( - roots := {name: dfn for name, dfn in dfns.items() if dfn.parent is None} - ) - ) != 1: + roots = {name: dfn for name, dfn in dfns.items() if dfn.parent is None} + if (nroots := len(roots)) != 1: raise ValueError(f"Expected one root component, found {nroots}") def _build_tree(node_name: str) -> Dfn: node = dfns[node_name] - children = {name: dfn for name, dfn in dfns.items() if dfn.parent == node_name} - if any(children): - node.children = {name: _build_tree(name) for name in children.keys()} + children = { + name: dfn + for name, dfn in dfns.items() + if dfn.parent == node_name + } + if children: + node = node.model_copy( + update={"children": {name: _build_tree(name) for name in children}} + ) return node return _build_tree(next(iter(roots.keys()))) case _: - raise ValueError(f"Unsupported schema version: {schema_version}. Expected 1 or 2.") + raise ValueError( + f"Unsupported schema version: {schema_version}. Expected 1 or 2." + ) def to_flat(dfn: Dfn) -> Dfns: """ - Flatten a MODFLOW 6 input component hierarchy to a flat spec: - unlinked DFNs, i.e. without `children` populated, only `parent`. + Flatten a MODFLOW 6 input component hierarchy to a flat spec. - Returns a dictionary of all components in the specification. + Returns a dictionary of all components without `children` populated. """ def _flatten(dfn: Dfn) -> Dfns: - dfns = {dfn.name: replace(dfn, children=None)} + result: Dfns = {dfn.name: dfn.model_copy(update={"children": None})} for child in (dfn.children or {}).values(): - dfns.update(_flatten(child)) - return dfns + result.update(_flatten(child)) + return result return _flatten(dfn) @@ -807,9 +1115,6 @@ def is_valid(path: str | PathLike, format: str = "dfn", verbose: bool = False) - # Registry imports and convenience functions # ============================================================================= -# Import registry classes and functions (lazy to avoid circular imports) -# These are re-exported for convenience - def _get_registry_module(): """Lazy import of registry module to avoid circular imports.""" @@ -818,7 +1123,6 @@ def _get_registry_module(): return registry -# Re-export registry classes def __getattr__(name: str): """Lazy attribute access for registry classes.""" registry_exports = { @@ -851,32 +1155,6 @@ def get_dfn( ) -> "Dfn": """ Get a DFN by component name from the registry. - - This is a convenience function that gets the registry and retrieves - the specified component. - - Parameters - ---------- - component : str - Component name (e.g., "gwf-chd", "sim-nam"). - ref : str, optional - Git ref (branch, tag, or commit hash). Default is "develop". - source : str, optional - Source repository name. Default is "modflow6". - path : str or PathLike, optional - Path to a local directory containing DFN files. If provided, - uses autodiscovery from local filesystem instead of remote. - - Returns - ------- - Dfn - The requested component definition. - - Examples - -------- - >>> dfn = get_dfn("gwf-chd") - >>> dfn = get_dfn("gwf-chd", ref="6.6.0") - >>> dfn = get_dfn("gwf-chd", path="/path/to/dfns") """ registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) @@ -891,28 +1169,6 @@ def get_dfn_path( ) -> Path: """ Get the local cached file path for a DFN component. - - Parameters - ---------- - component : str - Component name (e.g., "gwf-chd", "sim-nam"). - ref : str, optional - Git ref (branch, tag, or commit hash). Default is "develop". - source : str, optional - Source repository name. Default is "modflow6". - path : str or PathLike, optional - Path to a local directory containing DFN files. If provided, - returns path from local filesystem instead of cache. - - Returns - ------- - Path - Path to the local DFN file (cached or local directory). - - Examples - -------- - >>> path = get_dfn_path("gwf-chd", ref="6.6.0") - >>> path = get_dfn_path("gwf-chd", path="/path/to/dfns") """ registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) @@ -926,28 +1182,6 @@ def list_components( ) -> list[str]: """ List available components for a registry. - - Parameters - ---------- - ref : str, optional - Git ref (branch, tag, or commit hash). Default is "develop". - source : str, optional - Source repository name. Default is "modflow6". - path : str or PathLike, optional - Path to a local directory containing DFN files. If provided, - lists components from local filesystem. - - Returns - ------- - list[str] - List of component names available in the registry. - - Examples - -------- - >>> components = list_components(ref="6.6.0") - >>> "gwf-chd" in components - True - >>> components = list_components(path="/path/to/dfns") """ registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) diff --git a/modflow_devtools/dfns/dfn2toml.py b/modflow_devtools/dfns/dfn2toml.py index 33760280..10ebc71f 100644 --- a/modflow_devtools/dfns/dfn2toml.py +++ b/modflow_devtools/dfns/dfn2toml.py @@ -3,16 +3,24 @@ import argparse import sys import textwrap -from dataclasses import asdict from os import PathLike from pathlib import Path import tomli_w as tomli from boltons.iterutils import remap -from modflow_devtools.dfns import Dfn, is_valid, load, load_flat, map, to_flat, to_tree +from modflow_devtools.dfns import ( + Dfn, + _dfn_to_plain_dict, + _toml_safe, + is_valid, + load, + load_flat, + map, + to_flat, + to_tree, +) from modflow_devtools.dfns.parse import parse_dfn -from modflow_devtools.dfns.schema.block import block_sort_key from modflow_devtools.misc import drop_none_or_empty # mypy: ignore-errors @@ -55,26 +63,14 @@ def convert(inpath: PathLike, outdir: PathLike, schema_version: str = "2") -> No def _convert(dfn: Dfn, outpath: Path) -> None: with Path.open(outpath, "wb") as f: - # TODO if we start using c/attrs, swap out - # all this for a custom unstructuring hook - dfn_dict = asdict(dfn) - dfn_dict["schema_version"] = str(dfn_dict["schema_version"]) + dfn_dict = _dfn_to_plain_dict(dfn) if blocks := dfn_dict.pop("blocks", None): for block_name, block_fields in blocks.items(): - if block_name not in dfn_dict: - dfn_dict[block_name] = {} + dfn_dict.setdefault(block_name, {}) for field_name, field_data in block_fields.items(): dfn_dict[block_name][field_name] = field_data - tomli.dump( - dict( - sorted( - remap(dfn_dict, visit=drop_none_or_empty).items(), - key=block_sort_key, - ) - ), - f, - ) + tomli.dump(_toml_safe(remap(dfn_dict, visit=drop_none_or_empty)), f) if __name__ == "__main__": diff --git a/modflow_devtools/dfns/schema/block.py b/modflow_devtools/dfns/schema/block.py deleted file mode 100644 index b545a311..00000000 --- a/modflow_devtools/dfns/schema/block.py +++ /dev/null @@ -1,20 +0,0 @@ -from collections.abc import Mapping - -from modflow_devtools.dfns.schema.field import Fields - -Block = Fields -Blocks = Mapping[str, Block] - - -def block_sort_key(item) -> int: - k, _ = item - if k == "options": - return 0 - elif k == "dimensions": - return 1 - elif k == "griddata": - return 2 - elif "period" in k: - return 4 - else: - return 3 diff --git a/modflow_devtools/dfns/schema/field.py b/modflow_devtools/dfns/schema/field.py deleted file mode 100644 index 985df211..00000000 --- a/modflow_devtools/dfns/schema/field.py +++ /dev/null @@ -1,22 +0,0 @@ -from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any - -Fields = Mapping[str, "Field"] - - -@dataclass(kw_only=True) -class Field: - name: str - type: str | None = None - block: str | None = None - default: Any | None = None - longname: str | None = None - description: str | None = None - children: Fields | None = None - optional: bool = False - developmode: bool = False - shape: str | None = None - valid: tuple[str, ...] | None = None - netcdf: bool = False - tagged: bool = False diff --git a/modflow_devtools/dfns/schema/v1.py b/modflow_devtools/dfns/schema/v1.py index 5722771c..292fc52d 100644 --- a/modflow_devtools/dfns/schema/v1.py +++ b/modflow_devtools/dfns/schema/v1.py @@ -1,7 +1,5 @@ from dataclasses import dataclass -from typing import Literal - -from modflow_devtools.dfns.schema.field import Field +from typing import Any, Literal FieldType = Literal[ "keyword", @@ -25,10 +23,22 @@ @dataclass(kw_only=True) -class FieldV1(Field): +class FieldV1: + # Shared base attributes (inlined from the deleted field.py) + name: str + type: str | None = None + block: str | None = None + default: Any | None = None + longname: str | None = None + description: str | None = None + optional: bool = False + developmode: bool = False + shape: str | None = None valid: tuple[str, ...] | None = None - reader: Reader = "urword" + netcdf: bool = False tagged: bool = False + # V1-specific attributes + reader: Reader = "urword" in_record: bool = False layered: bool | None = None preserve_case: bool = False @@ -42,18 +52,7 @@ class FieldV1(Field): @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "FieldV1": - """ - Create a FieldV1 instance from a dictionary. - - Parameters - ---------- - d : dict - Dictionary containing field data - strict : bool, optional - If True, raise ValueError if dict contains unrecognized keys. - If False (default), ignore unrecognized keys. - """ - keys = set(list(cls.__annotations__.keys()) + list(Field.__annotations__.keys())) + keys = set(cls.__dataclass_fields__.keys()) if strict: if extra_keys := set(d.keys()) - keys: raise ValueError(f"Unrecognized keys in field data: {extra_keys}") diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema/v2.py index bf676e16..02de61de 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema/v2.py @@ -1,32 +1,843 @@ -from dataclasses import dataclass -from typing import Literal +import ast +import re +from collections.abc import Iterator, Mapping, Sequence +from os import PathLike +from typing import Annotated, Any, Literal -from modflow_devtools.dfns.schema.field import Field +from pydantic import BaseModel, ConfigDict, Field as PydanticField, GetCoreSchemaHandler, field_validator, model_validator +from pydantic_core import core_schema -FieldType = Literal["keyword", "integer", "double", "string", "record", "array", "list"] -SCALAR_TYPES = ("keyword", "integer", "double", "string") +class FieldBase(BaseModel): + model_config = ConfigDict(frozen=True) + @property + def children(self) -> "dict[str, Field] | None": + return None + + @classmethod + def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": + type_ = d.get("type") + type_map: dict[str | None, type[FieldBase]] = { + "keyword": Keyword, + "string": String, + "integer": Integer, + "double": Double, + "path": Path, + "array": Array, + "record": Record, + "union": Union, + "list": List, + } + target = type_map.get(type_) + if target is None: + raise ValueError(f"Unknown or missing field type: {type_!r}") + if strict: + extra = set(d.keys()) - set(target.model_fields.keys()) + if extra: + raise ValueError(f"Unrecognized keys in field data: {extra}") + return target.model_validate(d) + + +class Keyword(FieldBase): + type: Literal["keyword"] = "keyword" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False -@dataclass(kw_only=True) -class FieldV2(Field): - pass +class String(FieldBase): + type: Literal["string"] = "string" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + tagged: bool = True + valid: list[str] | None = None + case_sensitive: bool = False + time_series: bool = False + pk: bool = False + fk: str | None = None + fk_ref: str | None = None + + +class Integer(FieldBase): + type: Literal["integer"] = "integer" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + tagged: bool = True + valid: list[int] | None = None + dimension: Literal["record", "component", "model", "simulation"] | None = None + time_series: bool = False + pk: bool = False + fk: str | None = None + fk_ref: str | None = None + + @field_validator("dimension", mode="before") @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> "FieldV2": + def _coerce_dimension(cls, v: Any) -> Any: + if v is True: + return "component" + if v is False: + return None + return v + + +class Double(FieldBase): + type: Literal["double"] = "double" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + tagged: bool = True + time_series: bool = False + + +class Path(FieldBase): + type: Literal["path"] = "path" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + mode: Literal["filein", "fileout"] + + +Scalar = Annotated[ + Keyword | String | Integer | Double | Path, + PydanticField(discriminator="type"), +] + + +class Array(FieldBase): + type: Literal["array"] = "array" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + dtype: Literal["keyword", "integer", "double", "string"] + shape: list[str] = [] + time_series: bool = False + repeat: str | None = None + dimension: Literal["record", "component", "model", "simulation"] | None = None + + @field_validator("dimension", mode="before") + @classmethod + def _coerce_dimension(cls, v: Any) -> Any: + if v is True: + return "component" + if v is False: + return None + return v + + @model_validator(mode="after") + def _validate_dimension(self) -> "Array": + if self.dimension is not None and self.dtype != "string": + raise ValueError( + f"Array {self.name!r}: dimension may only be set when dtype='string'" + ) + return self + + +class Record(FieldBase, Mapping): + type: Literal["record"] = "record" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + fields: "dict[str, Scalar | Array | Record | Union]" = PydanticField(default_factory=dict) + + @property + def children(self) -> "dict[str, Field]": + return self.fields # type: ignore[return-value] + + def __getitem__(self, key: str) -> Field: + return self.children[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.children) + + def __len__(self) -> int: + return len(self.children) + + +class Union(FieldBase, Mapping): + type: Literal["union"] = "union" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + arms: "dict[str, Scalar | Array | Record]" = PydanticField(default_factory=dict) + + @property + def children(self) -> "dict[str, Field]": + return self.arms # type: ignore[return-value] + + def __getitem__(self, key: str) -> Field: + return self.children[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.children) + + def __len__(self) -> int: + return len(self.children) + + +class List(FieldBase, Sequence): + type: Literal["list"] = "list" + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + item: "Record | Union" + + @property + def children(self) -> "dict[str, Field]": + return {"item": self.item} # type: ignore[return-value] + + def __getitem__(self, key: str) -> Field: + return self.children[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.children) + + def __len__(self) -> int: + return len(self.children) + + +Field = Annotated[ + Keyword | String | Integer | Double | Path | Array | Record | Union | List, + PydanticField(discriminator="type"), +] + +# Backward-compat alias: all concrete v2 field types are FieldBase subclasses, +# so isinstance(field, FieldV2) remains True for any v2 field instance. +FieldV2 = FieldBase + +Record.model_rebuild() +Union.model_rebuild() +List.model_rebuild() + + +# Fallback set of well-known grid dim names used by grid_dims_for and the v1 +# mapper (map_period_block). Once all dims in the v1 corpus carry explicit +# "model" scope, this constant becomes unnecessary and will be removed. +GRID_DIM_NAMESPACE: frozenset[str] = frozenset({ + "nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert" +}) + + +def _collect_explicit_dims(component: "ComponentBase") -> set[str]: + """ + Gather all explicitly declared dimension names from a component. + + Collects Integer fields with ``dimension=True`` and string Array fields + with ``dimension=True``, recursing into Records, Union arms, and List + item records at any nesting depth. + """ + dims: set[str] = set() + + _GLOBAL_SCOPES = ("component", "model", "simulation") + + def _scan(fields: "dict[str, Any]") -> None: + for f in fields.values(): + if isinstance(f, Integer) and f.dimension in _GLOBAL_SCOPES: + dims.add(f.name) + elif isinstance(f, Array) and f.dtype == "string" and f.dimension in _GLOBAL_SCOPES: + dims.add(f.name) + elif isinstance(f, Record): + _scan(f.fields) + elif isinstance(f, Union): + _scan(f.arms) + elif isinstance(f, List): + item = f.item + _scan(item.fields if isinstance(item, Record) else item.arms) + + for block in (component.blocks or {}).values(): + _scan(block.fields) + return dims + + +def _names_in_expr(expr: str) -> set[str]: + """Return Name identifiers from expr, excluding those inside sum() calls.""" + try: + tree = ast.parse(expr, mode="eval") + except SyntaxError as e: + raise ValueError(f"Invalid expression {expr!r}: {e}") from e + + sum_interior_ids: set[int] = set() + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "sum" + ): + for child in ast.walk(node): + if child is not node: + sum_interior_ids.add(id(child)) + + return { + node.id + for node in ast.walk(tree) + if isinstance(node, ast.Name) and id(node) not in sum_interior_ids + } + + +def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> None: + """Validate a sum(list.col) or sum(block.list.col) call in a derived_dims expression.""" + if len(call.args) != 1: + raise ValueError( + f"sum() in derived_dims must have exactly one argument in {expr!r}" + ) + arg = call.args[0] + if not isinstance(arg, ast.Attribute): + raise ValueError(f"sum() argument must be an attribute expression in {expr!r}") + + col_name = arg.attr + if isinstance(arg.value, ast.Name): + list_name = arg.value.id + block_qualifier: str | None = None + elif isinstance(arg.value, ast.Attribute) and isinstance(arg.value.value, ast.Name): + block_qualifier = arg.value.value.id + list_name = arg.value.attr + else: + raise ValueError(f"Unrecognised sum() form in {expr!r}") + + found_block: str | None = None + found_list: "List | None" = None + for block_name, block in (component.blocks or {}).items(): + f = block.fields.get(list_name) + if isinstance(f, List): + found_block = block_name + found_list = f + break + + if found_list is None: + raise ValueError( + f"sum() references unknown list field {list_name!r} in {expr!r}" + ) + if block_qualifier is not None and block_qualifier != found_block: + raise ValueError( + f"sum() block qualifier {block_qualifier!r} does not match " + f"actual block {found_block!r} in {expr!r}" + ) + + item = found_list.item + item_fields: dict = item.fields if isinstance(item, Record) else item.arms + col_field = item_fields.get(col_name) + if col_field is None: + raise ValueError( + f"sum() column {col_name!r} not found in {list_name!r} item fields in {expr!r}" + ) + if not isinstance(col_field, Integer): + raise ValueError( + f"sum() column {col_name!r} is {type(col_field).__name__}, " + f"must be Integer in {expr!r}" + ) + + +def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> list[str]: + """ + Validate derived_dims expressions and return their names in topological order. + Raises ValueError on cycles or unresolvable operands. + + ``known_dims`` is the full set of dim names visible to this component + (explicit + derived + inherited); pass ``_known_dims_for(spec, name)`` from + ``DfnSpec._validate_dims_and_shapes``, or an explicit set in tests. + """ + derived = component.derived_dims or {} + if not derived: + return [] + + derived_names = set(derived.keys()) + deps: dict[str, set[str]] = {} + + for name, expr in derived.items(): + try: + tree = ast.parse(expr, mode="eval") + except SyntaxError as e: + raise ValueError(f"Invalid derived_dims {name!r}: {expr!r}: {e}") from e + + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "sum" + ): + _validate_sum_call(node, component, expr) + + operands = _names_in_expr(expr) + for op in operands: + if op not in known_dims and op not in derived_names: + raise ValueError( + f"derived_dims {name!r} operand {op!r} is not a known dimension" + ) + deps[name] = operands & derived_names + + in_degree = {n: 0 for n in derived_names} + dependents: dict[str, set[str]] = {n: set() for n in derived_names} + for name, dep_set in deps.items(): + for dep in dep_set: + in_degree[name] += 1 + dependents[dep].add(name) + + queue = [n for n, d in in_degree.items() if d == 0] + order: list[str] = [] + while queue: + n = queue.pop(0) + order.append(n) + for dependent in dependents[n]: + in_degree[dependent] -= 1 + if in_degree[dependent] == 0: + queue.append(dependent) + + if len(order) != len(derived_names): + cyclic = {n for n, d in in_degree.items() if d > 0} + raise ValueError(f"Cycle in derived_dims: {cyclic}") + + return order + + +class Block(BaseModel, Mapping): + model_config = ConfigDict(frozen=True) + name: str + fields: dict[str, Field] + repeats: bool = False + optional: bool = False + + @field_validator("fields", mode="before") + @classmethod + def _coerce_field_instances(cls, v: Any) -> Any: + if isinstance(v, dict): + return { + k: (val.model_dump() if isinstance(val, FieldBase) else val) + for k, val in v.items() + } + return v + + def __getitem__(self, key: str) -> Field: + return self.fields[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.fields) + + def __len__(self) -> int: + return len(self.fields) + + +Blocks = Mapping[str, Block] + + +from packaging.version import Version +from pydantic import GetCoreSchemaHandler +from pydantic_core import core_schema + +class _VersionPydanticAnnotation: + @classmethod + def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler): + return core_schema.no_info_plain_validator_function( + lambda v: Version(str(v)) if not isinstance(v, Version) else v, + serialization=core_schema.to_string_ser_schema(), + ) + +VersionField = Annotated[Version, _VersionPydanticAnnotation] + + +class ComponentBase(BaseModel): + model_config = ConfigDict(frozen=True) + name: str + blocks: dict[str, Block] | None = None + parent: str | list[str] | None = None + schema_version: VersionField | None = None + derived_dims: dict[str, str] | None = None + + +class Simulation(ComponentBase): + type: Literal["simulation"] = "simulation" + +class Model(ComponentBase): + type: Literal["model"] = "model" + solution: str | list[str] | None = None # compatible solution type(s) + +class Package(ComponentBase): + type: Literal["package"] = "package" + multi: bool = False + subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = None + variant_of: str | None = None + + +Component = Annotated[ + Simulation | Model | Package, + PydanticField(discriminator="type"), +] + +# Shape element patterns +_DIM_RE = re.compile(r"^[A-Za-z_]\w*$") +_LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") + + +def _known_dims_for(spec: "DfnSpec", component_name: str) -> set[str]: + """ + Return the full set of dim names valid for shape references in a component. + Scope chain (levels 1-3; level 4 is intra-record sibling, checked per-field): + 1. local explicit dims: Integer(dimension=True) and Array(dtype="string", dimension=True) + 2. local derived dims (keys of component.derived_dims) + 3. inherited grid dims (from all other components in the spec) + """ + component = spec.components[component_name] + return ( + _collect_explicit_dims(component) + | set((component.derived_dims or {}).keys()) + | spec.grid_dims_for(component_name) + ) + + +def _find_list_in_block(component: "ComponentBase", block_name: str) -> "List | None": + """Return the first List field in the named block, or None.""" + block = (component.blocks or {}).get(block_name) + if block is None: + return None + for f in block.fields.values(): + if isinstance(f, List): + return f + return None + + +def _validate_shape_element( + element: str, + array_field: "Array", + component: "ComponentBase", + enclosing_record: "Record | None", + known_dims: set[str], +) -> None: + """ + Validate one element of an Array.shape list. + + Valid forms: + - Dim reference ``^[A-Za-z_]\\w*$`` + Must resolve in the 3-level scope: explicit → derived → grid dims. + - Row-level column lookup ``^(\\w+)\\.(\\w+)\\((\\w+)\\)$`` + Structural checks (see plan §Shape element parsing). + + Raises ValueError on any violation. + """ + if _DIM_RE.fullmatch(element): + if element in known_dims: + return + # Per-row varying shape: a sibling field in the same enclosing record + # supplies the inline count for this row. Valid when the sibling has + # dimension="record" (or, for string Arrays, is a record-scoped dim). + if enclosing_record is not None: + sibling = enclosing_record.fields.get(element) + if isinstance(sibling, (Integer, Array)) and sibling.dimension == "record": + return + raise ValueError( + f"Array {array_field.name!r} shape element {element!r} " + f"does not resolve to a known dim " + f"(explicit, derived, or grid)" + ) + return + + if m := _LOOKUP_RE.fullmatch(element): + block_name, col_name, fk_field_name = m.groups() + + # Check 5: array must be a subfield of a record, not a top-level block field + if enclosing_record is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r} is a " + f"row-level lookup but the array is not inside a record" + ) + + # Check 1: block_name must identify a list block in this component + list_field = _find_list_in_block(component, block_name) + if list_field is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{block_name!r} is not a list block in this component" + ) + + # Check 2: col_name must be an Integer field in the list's item record + item = list_field.item + item_fields: dict = item.fields if isinstance(item, Record) else item.arms + col_field = item_fields.get(col_name) + if col_field is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{col_name!r} is not a field in {list_field.name!r} item" + ) + if not isinstance(col_field, Integer): + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{col_name!r} is {type(col_field).__name__}, must be Integer" + ) + + # Check 3: fk_field_name must be a sibling field in the enclosing record + fk_field = enclosing_record.fields.get(fk_field_name) + if fk_field is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{fk_field_name!r} is not a sibling field in the enclosing record" + ) + + # Check 4: fk_field.fk must be set and its block portion must match block_name + fk = getattr(fk_field, "fk", None) + if fk is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{fk_field_name!r}.fk is not set" + ) + fk_block = fk.split(".")[0] if "." in fk else fk + if fk_block != block_name: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{fk_field_name!r}.fk = {fk!r} does not reference block {block_name!r}" + ) + return + + raise ValueError( + f"Array {array_field.name!r} has invalid shape element {element!r}: " + f"must be a dim reference (^[A-Za-z_]\\w*$) or a row-level " + f"lookup (block.column(fk_field))" + ) + + +def _validate_fk_fields(component: "ComponentBase", spec: "DfnSpec") -> None: + """ + For every Integer/String field with fk or fk_ref set, validate structural + consistency: + - fk must reference a list block in this component, and that list's item + must have at least one pk=True field. + - fk_ref must name a component that exists in the spec. + """ + if not component.blocks: + return + + 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) + + if fk is not None: + block_name = fk.split(".")[0] if "." in fk else fk + list_field = _find_list_in_block(component, block_name) + if list_field is None: + raise ValueError( + f"Field {field.name!r} fk={fk!r}: " + f"{block_name!r} is not a list block in this component" + ) + item = list_field.item + item_fields: dict = ( + item.fields if isinstance(item, Record) else item.arms + ) + has_pk = any(getattr(f, "pk", False) for f in item_fields.values()) + if not has_pk: + raise ValueError( + f"Field {field.name!r} fk={fk!r}: " + f"list {list_field.name!r} item has no pk=True field" + ) + + if fk_ref is not None and fk_ref not in spec.components: + raise ValueError( + f"Field {field.name!r} fk_ref={fk_ref!r}: " + f"component {fk_ref!r} not found in spec" + ) + + if isinstance(field, Record): + _check_fields(field.fields) + elif isinstance(field, Union): + _check_fields(field.arms) + elif isinstance(field, List): + item = field.item + if isinstance(item, Record): + _check_fields(item.fields) + + for block in component.blocks.values(): + _check_fields(block.fields) + + +def _validate_array_shapes( + component: "ComponentBase", + component_name: str, + spec: "DfnSpec", +) -> None: + """ + Validate all Array.shape elements in a component. + + Arrays are found at three nesting levels: + - Top-level block fields (no enclosing record) + - Fields within a top-level Record (enclosing_record = the Record) + - Fields within a List item Record (enclosing_record = the item Record) + """ + if not component.blocks: + return + + known_dims = _known_dims_for(spec, component_name) + + def _check_array(arr: "Array", enclosing: "Record | None") -> None: + if arr.dtype == "string": + return # inline string arrays are self-sizing; no declared dim needed + for elem in arr.shape: + _validate_shape_element(elem, arr, component, enclosing, known_dims) + + for block in component.blocks.values(): + for field in block.fields.values(): + if isinstance(field, Array): + _check_array(field, None) + + elif isinstance(field, Record): + for subfield in field.fields.values(): + if isinstance(subfield, Array): + _check_array(subfield, field) + + elif isinstance(field, List): + item = field.item + if isinstance(item, Record): + for subfield in item.fields.values(): + if isinstance(subfield, Array): + _check_array(subfield, item) + + +class DfnSpec(BaseModel, Mapping): + model_config = ConfigDict(frozen=True) + components: dict[str, Component] + + # ── Mapping protocol ───────────────────────────────────────────────────── + + def __getitem__(self, key: str) -> Component: + return self.components[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.components) + + def __len__(self) -> int: + return len(self.components) + + # ── Properties ─────────────────────────────────────────────────────────── + + @property + def schema_version(self) -> Version: + for c in self.components.values(): + if c.schema_version is not None: + return c.schema_version + return Version("2") + + @property + def root(self) -> "Simulation | None": + """Return the single Simulation component, or None if not present.""" + for c in self.components.values(): + if isinstance(c, Simulation): + return c + return None + + # ── Query helpers ───────────────────────────────────────────────────────── + + def children_of(self, name: str) -> "dict[str, Component]": + """Return all components whose parent matches `name`.""" + return {n: c for n, c in self.components.items() if c.parent == name} + + def explicit_dims_for(self, component_name: str) -> set[str]: + """Return the set of explicit dim names for a component.""" + return _collect_explicit_dims(self.components[component_name]) + + def grid_dims_for(self, component_name: str) -> set[str]: """ - Create a FieldV2 instance from a dictionary. - - Parameters - ---------- - d : dict - Dictionary containing field data - strict : bool, optional - If True, raise ValueError if dict contains unrecognized keys. - If False (default), ignore unrecognized keys. + Return dim names inherited by ``component_name`` from the rest of the spec. + + For v1-mapped specs (no explicit parent chain), this is a permissive + superset: it scans every other component for explicit dims (any scope + except "record") and derived dim names, then unions in + ``GRID_DIM_NAMESPACE`` as a fallback for dims not yet explicitly scoped + in the corpus. Native v2 specs with ``parent`` populated will + eventually use exact parent-chain resolution instead. """ - keys = set(list(cls.__annotations__.keys()) + list(Field.__annotations__.keys())) - if strict: - if extra_keys := set(d.keys()) - keys: - raise ValueError(f"Unrecognized keys in field data: {extra_keys}") - return cls(**{k: v for k, v in d.items() if k in keys}) + dims: set[str] = set(GRID_DIM_NAMESPACE) + for name, c in self.components.items(): + if name != component_name: + dims |= _collect_explicit_dims(c) + dims |= set((c.derived_dims or {}).keys()) + return dims + + # ── Validation ──────────────────────────────────────────────────────────── + + @model_validator(mode="after") + def _validate_dims_and_shapes(self) -> "DfnSpec": + """ + At construction time, for every component: + 1. Validate derived_dims expressions (topological sort, operand scope). + 2. Validate every Array.shape element (dim reference or row-level lookup). + Shape validation runs after dims so the derived dim names are available + as part of the known scope when checking dim references. + """ + for name, component in self.components.items(): + if component.derived_dims: + _resolve_derived_dims(component, _known_dims_for(self, name)) + for name, component in self.components.items(): + _validate_fk_fields(component, self) + for name, component in self.components.items(): + _validate_array_shapes(component, name, self) + return self + + # ── Loading ─────────────────────────────────────────────────────────────── + + @classmethod + def load( + cls, + path: "str | PathLike", + schema_version: "str | Version | None" = None, + ) -> "DfnSpec": + """ + Load a DfnSpec from a directory of DFN or TOML files. + + Component types are inferred from component names using the MF6 + naming conventions. This is a transitional implementation; when the + v1→v2 mapper is complete it will be replaced by a proper mapper call. + """ + from pathlib import Path as _Path + + from modflow_devtools.dfns import ( + MapV1To2, + _apply_parent_inference, + load_flat, + ) + from modflow_devtools.dfns import map as map_dfn + + _path = _Path(path).expanduser().resolve() + dfns = load_flat(_path) + if not dfns: + raise ValueError(f"No DFN files found in {_path}") + + first = next(iter(dfns.values())) + if first.schema_version == Version("1"): + dfns = _apply_parent_inference(dfns) + dfns = {n: map_dfn(d, "2") for n, d in dfns.items()} + + components: dict[str, Component] = { + name: MapV1To2.to_component(dfn) for name, dfn in dfns.items() + } + return cls(components=components) \ No newline at end of file From dc06561b4b0db60cbee5464d38f89df69c89b3ea Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 11 May 2026 14:17:33 -0700 Subject: [PATCH 02/29] ruff --- autotest/test_dfns.py | 2 - autotest/test_dfns_schema.py | 22 ++--- modflow_devtools/dfns/__init__.py | 140 ++++++++++++++--------------- modflow_devtools/dfns/schema/v2.py | 75 +++++++--------- 4 files changed, 105 insertions(+), 134 deletions(-) diff --git a/autotest/test_dfns.py b/autotest/test_dfns.py index a1fb7acf..a80b577d 100644 --- a/autotest/test_dfns.py +++ b/autotest/test_dfns.py @@ -11,12 +11,10 @@ Array, Double, FieldBase, - FieldV2, Integer, Keyword, Record, String, - Union, ) from modflow_devtools.markers import requires_pkg diff --git a/autotest/test_dfns_schema.py b/autotest/test_dfns_schema.py index 762d7053..2ad216ad 100644 --- a/autotest/test_dfns_schema.py +++ b/autotest/test_dfns_schema.py @@ -7,18 +7,15 @@ from modflow_devtools.dfns.schema.v2 import ( Array, Block, - ComponentBase, DfnSpec, Double, Integer, - Keyword, List, Model, Package, Record, Simulation, String, - Union, _collect_explicit_dims, _known_dims_for, _names_in_expr, @@ -28,7 +25,6 @@ _validate_sum_call, ) - # ── Helpers ─────────────────────────────────────────────────────────────────── @@ -41,9 +37,7 @@ def _dim_block(*names: str) -> Block: def _pkg(name: str, blocks=None, derived_dims=None, parent=None, **kw) -> Package: - return Package( - name=name, blocks=blocks, derived_dims=derived_dims, parent=parent, **kw - ) + return Package(name=name, blocks=blocks, derived_dims=derived_dims, parent=parent, **kw) # ── _collect_explicit_dims ──────────────────────────────────────────────────── @@ -417,9 +411,7 @@ def test_dfnspec_children_of(): chd = _pkg("gwf-chd", parent="gwf-nam") rch = _pkg("gwf-rch", parent="gwf-nam") sim = Simulation(name="sim-nam", blocks=None) - spec = DfnSpec( - components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch} - ) + spec = DfnSpec(components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch}) children = spec.children_of("gwf-nam") assert set(children) == {"gwf-chd", "gwf-rch"} @@ -491,8 +483,8 @@ def test_known_dims_includes_grid_dims(): chd = _pkg("gwf-chd", parent="gwf-nam") spec2 = DfnSpec(components=dict(spec.components) | {"gwf-chd": chd}) known = _known_dims_for(spec2, "gwf-chd") - assert "nodes" in known # GRID_DIM_NAMESPACE - assert "nlay" in known # from gwf-dis (sibling dis package) + assert "nodes" in known # GRID_DIM_NAMESPACE + assert "nlay" in known # from gwf-dis (sibling dis package) # ── _validate_shape_element: dim reference ──────────────────────────────────── @@ -521,9 +513,7 @@ def test_shape_element_valid_grid_dim(): def test_shape_element_valid_derived_dim(): - arr, pkg, known = _make_ctx( - {"nlay", "nrow", "ncol"}, derived={"nodes": "nlay * nrow * ncol"} - ) + arr, pkg, known = _make_ctx({"nlay", "nrow", "ncol"}, derived={"nodes": "nlay * nrow * ncol"}) _validate_shape_element("nodes", arr, pkg, None, known) @@ -585,7 +575,7 @@ def test_shape_element_valid_row_level_lookup(): def test_shape_element_lookup_on_top_level_array_raises(): - arr, enc, pkg, known = _lookup_ctx() + arr, _enc, pkg, known = _lookup_ctx() with pytest.raises(ValueError, match="not inside a record"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, pkg, None, known) diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index 98146a26..99e7be5a 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -16,8 +16,6 @@ cast, ) -_IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") - import tomli from boltons.dictutils import OMD from packaging.version import Version @@ -45,13 +43,17 @@ Integer, Keyword, List, - Path as PathField, Record, String, Union, ) +from modflow_devtools.dfns.schema.v2 import ( + Path as PathField, +) from modflow_devtools.misc import try_literal_eval +_IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") + # Experimental API warning warnings.warn( "The modflow_devtools.dfns API is experimental and may change or be " @@ -112,9 +114,7 @@ class _VersionAnnotation: @classmethod - def __get_pydantic_core_schema__( - cls, source: Any, handler: GetCoreSchemaHandler - ) -> Any: + def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> Any: return core_schema.no_info_plain_validator_function( lambda v: Version(str(v)) if not isinstance(v, Version) else v, serialization=core_schema.to_string_ser_schema(), @@ -209,9 +209,7 @@ def from_dict(cls, d: dict, strict: bool = False) -> "Dfn": for field_name, field_data in block_data.items(): if isinstance(field_data, dict): if is_v1: - block_fields[field_name] = FieldV1.from_dict( - field_data, strict=strict - ) + block_fields[field_name] = FieldV1.from_dict(field_data, strict=strict) else: block_fields[field_name] = FieldBase.from_dict( field_data, strict=strict @@ -267,9 +265,7 @@ def map_period_block(dfn: "Dfn", block: dict) -> dict: list_field: List = fields_list[0] block.pop(list_field.name) item = list_field.item - columns: dict = dict( - item.fields if isinstance(item, Record) else item.arms - ) + columns: dict = dict(item.fields if isinstance(item, Record) else item.arms) else: columns = dict(block) @@ -290,6 +286,7 @@ def map_period_block(dfn: "Dfn", block: dict) -> dict: continue from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + old_dims = list(column.shape) if isinstance(column, Array) else [] new_dims = ["nper"] if cellid: @@ -347,19 +344,17 @@ def _map_field(f: FieldV1) -> FieldBase: time_series: bool = _to_bool(fd.get("time_series"), False) valid = fd.get("valid") default = ( - try_literal_eval(fd.get("default")) - if _type != "string" - else fd.get("default") + try_literal_eval(fd.get("default")) if _type != "string" else fd.get("default") ) - common = dict( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - ) + common = { + "name": _name, + "longname": longname, + "description": description, + "optional": optional, + "default": default, + "developmode": developmode, + } _COL_FK_RE = re.compile(r"^([A-Za-z_]\w*)\(([A-Za-z_]\w*)\)$") @@ -375,7 +370,9 @@ def _parse_shape(s: str) -> list[str]: # v1 discretization-conditional (e.g. "ncol*nrow; ncpl") # → canonical per-layer count; DIS derives ncpl = nrow*ncol. result.append("ncpl") - elif elem in ("any1d", "unknown") or elem.startswith("<") or elem.startswith(">"): + elif ( + elem in ("any1d", "unknown") or elem.startswith("<") or elem.startswith(">") + ): # v1 pseudo-elements with no v2 shape equivalent: # any1d — inline array of runtime-determined length # (read to end of record); dtype-agnostic. @@ -387,10 +384,11 @@ def _parse_shape(s: str) -> list[str]: # Resolve the block by searching for the integer field. col_name = m.group(1) block_name = next( - (fi.block for fi in fields.values(multi=True) - if fi.name == col_name - and fi.type == "integer" - and fi.in_record), + ( + fi.block + for fi in fields.values(multi=True) + if fi.name == col_name and fi.type == "integer" and fi.in_record + ), None, ) if block_name: @@ -402,9 +400,12 @@ def _parse_shape(s: str) -> list[str]: # string array's name so _mark_string_dim_arrays can mark # it dimension="component" and validation resolves it. provider = next( - (fi.name for fi in fields.values(multi=True) - if fi.type == "string" - and (fi.shape or "").strip() in (f"({elem})", elem)), + ( + fi.name + for fi in fields.values(multi=True) + if fi.type == "string" + and (fi.shape or "").strip() in (f"({elem})", elem) + ), None, ) result.append(provider if provider else elem) @@ -425,6 +426,7 @@ def _to_scalar() -> FieldBase: ) if _type == "integer": from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + v = [int(x) for x in valid] if valid else None if fd.get("block") == "dimensions": if _name in GRID_DIM_NAMESPACE: @@ -444,9 +446,7 @@ def _to_scalar() -> FieldBase: dimension=_dim_scope, ) if _type in ("double", "double precision"): - return Double( - **common, netcdf=netcdf, tagged=tagged, time_series=time_series - ) + return Double(**common, netcdf=netcdf, tagged=tagged, time_series=time_series) raise TypeError(f"Unsupported scalar type: {_type!r}") def _row_field() -> "Record | Union": @@ -461,13 +461,15 @@ def _row_field() -> "Record | Union": ] # Single explicit record or keystring - if len(item_names) == 1 and item_types and ( - (item_types[0] or "").startswith("record") - or (item_types[0] or "").startswith("keystring") - ): - mapped = MapV1To2.map_field( - dfn, next(iter(fields.getlist(item_names[0]))) + if ( + len(item_names) == 1 + and item_types + and ( + (item_types[0] or "").startswith("record") + or (item_types[0] or "").startswith("keystring") ) + ): + mapped = MapV1To2.map_field(dfn, next(iter(fields.getlist(item_names[0])))) if isinstance(mapped, (Record, Union)): return mapped raise TypeError( @@ -498,8 +500,7 @@ def _row_field() -> "Record | Union": return Record( name=_name, description=( - (description or "").replace("is the list of", "is the record of") - or None + (description or "").replace("is the list of", "is the record of") or None ), fields=children, ) @@ -659,9 +660,11 @@ def _mark(fields: dict) -> dict: new_fields = _mark(f.fields) if local_dims: new_fields = { - fn: (sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf) + fn: ( + sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf + ) for fn, sf in new_fields.items() } f = f.model_copy(update={"fields": new_fields}) @@ -674,9 +677,11 @@ def _mark(fields: dict) -> dict: new_item_fields = _mark(item.fields) if local_dims: new_item_fields = { - fn: (sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf) + fn: ( + sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf + ) for fn, sf in new_item_fields.items() } new_item = item.model_copy(update={"fields": new_item_fields}) @@ -840,27 +845,23 @@ def to_component(dfn: "Dfn") -> "Any": ) name = dfn.name - blocks: "dict[str, Block] | None" = None + blocks: dict[str, Block] | None = None if dfn.blocks: blocks = { block_name: Block( name=block_name, - fields={ - k: v - for k, v in block_fields.items() - if isinstance(v, FieldBase) - }, + fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, ) for block_name, block_fields in dfn.blocks.items() if isinstance(block_fields, dict) } - common: dict[str, Any] = dict( - name=name, - blocks=blocks, - parent=dfn.parent, - schema_version=dfn.schema_version, - ) + common: dict[str, Any] = { + "name": name, + "blocks": blocks, + "parent": dfn.parent, + "schema_version": dfn.schema_version, + } if name == "sim-nam": return Simulation(**common) if name.endswith("-nam"): @@ -911,10 +912,7 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn: name = kwargs.pop("name") fields_parsed, meta = parse_dfn(f, **kwargs) blocks = { - block_name: { - field_dict["name"]: FieldV1.from_dict(field_dict) - for field_dict in block - } + block_name: {field_dict["name"]: FieldV1.from_dict(field_dict) for field_dict in block} for block_name, block in groupby( fields_parsed.values(multi=True), lambda fd: fd["block"] ) @@ -945,9 +943,7 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn: if (expected_name := kwargs.pop("name", None)) is not None: if dfn_fields["name"] != expected_name: - raise ValueError( - f"DFN name mismatch: {expected_name} != {dfn_fields['name']}" - ) + raise ValueError(f"DFN name mismatch: {expected_name} != {dfn_fields['name']}") blocks = {} for section_name, section_data in data.items(): @@ -1052,11 +1048,7 @@ def to_tree(dfns: Dfns) -> Dfn: def _build_tree(node_name: str) -> Dfn: node = dfns[node_name] - children = { - name: dfn - for name, dfn in dfns.items() - if dfn.parent == node_name - } + children = {name: dfn for name, dfn in dfns.items() if dfn.parent == node_name} if children: node = node.model_copy( update={"children": {name: _build_tree(name) for name in children}} @@ -1065,9 +1057,7 @@ def _build_tree(node_name: str) -> Dfn: return _build_tree(next(iter(roots.keys()))) case _: - raise ValueError( - f"Unsupported schema version: {schema_version}. Expected 1 or 2." - ) + raise ValueError(f"Unsupported schema version: {schema_version}. Expected 1 or 2.") def to_flat(dfn: Dfn) -> Dfns: diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema/v2.py index 02de61de..e6628b8a 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema/v2.py @@ -4,7 +4,17 @@ from os import PathLike from typing import Annotated, Any, Literal -from pydantic import BaseModel, ConfigDict, Field as PydanticField, GetCoreSchemaHandler, field_validator, model_validator +from packaging.version import Version +from pydantic import ( + BaseModel, + ConfigDict, + GetCoreSchemaHandler, + field_validator, + model_validator, +) +from pydantic import ( + Field as PydanticField, +) from pydantic_core import core_schema @@ -152,9 +162,7 @@ def _coerce_dimension(cls, v: Any) -> Any: @model_validator(mode="after") def _validate_dimension(self) -> "Array": if self.dimension is not None and self.dtype != "string": - raise ValueError( - f"Array {self.name!r}: dimension may only be set when dtype='string'" - ) + raise ValueError(f"Array {self.name!r}: dimension may only be set when dtype='string'") return self @@ -171,8 +179,8 @@ class Record(FieldBase, Mapping): @property def children(self) -> "dict[str, Field]": return self.fields # type: ignore[return-value] - - def __getitem__(self, key: str) -> Field: + + def __getitem__(self, key: str) -> "Field": return self.children[key] def __iter__(self) -> Iterator[str]: @@ -195,8 +203,8 @@ class Union(FieldBase, Mapping): @property def children(self) -> "dict[str, Field]": return self.arms # type: ignore[return-value] - - def __getitem__(self, key: str) -> Field: + + def __getitem__(self, key: str) -> "Field": return self.children[key] def __iter__(self) -> Iterator[str]: @@ -220,8 +228,8 @@ class List(FieldBase, Sequence): @property def children(self) -> "dict[str, Field]": return {"item": self.item} # type: ignore[return-value] - - def __getitem__(self, key: str) -> Field: + + def __getitem__(self, key: str) -> "Field": return self.children[key] def __iter__(self) -> Iterator[str]: @@ -248,9 +256,9 @@ def __len__(self) -> int: # Fallback set of well-known grid dim names used by grid_dims_for and the v1 # mapper (map_period_block). Once all dims in the v1 corpus carry explicit # "model" scope, this constant becomes unnecessary and will be removed. -GRID_DIM_NAMESPACE: frozenset[str] = frozenset({ - "nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert" -}) +GRID_DIM_NAMESPACE: frozenset[str] = frozenset( + {"nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert"} +) def _collect_explicit_dims(component: "ComponentBase") -> set[str]: @@ -293,11 +301,7 @@ def _names_in_expr(expr: str) -> set[str]: sum_interior_ids: set[int] = set() for node in ast.walk(tree): - if ( - isinstance(node, ast.Call) - and isinstance(node.func, ast.Name) - and node.func.id == "sum" - ): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "sum": for child in ast.walk(node): if child is not node: sum_interior_ids.add(id(child)) @@ -312,9 +316,7 @@ def _names_in_expr(expr: str) -> set[str]: def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> None: """Validate a sum(list.col) or sum(block.list.col) call in a derived_dims expression.""" if len(call.args) != 1: - raise ValueError( - f"sum() in derived_dims must have exactly one argument in {expr!r}" - ) + raise ValueError(f"sum() in derived_dims must have exactly one argument in {expr!r}") arg = call.args[0] if not isinstance(arg, ast.Attribute): raise ValueError(f"sum() argument must be an attribute expression in {expr!r}") @@ -330,7 +332,7 @@ def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> raise ValueError(f"Unrecognised sum() form in {expr!r}") found_block: str | None = None - found_list: "List | None" = None + found_list: List | None = None for block_name, block in (component.blocks or {}).items(): f = block.fields.get(list_name) if isinstance(f, List): @@ -339,9 +341,7 @@ def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> break if found_list is None: - raise ValueError( - f"sum() references unknown list field {list_name!r} in {expr!r}" - ) + raise ValueError(f"sum() references unknown list field {list_name!r} in {expr!r}") if block_qualifier is not None and block_qualifier != found_block: raise ValueError( f"sum() block qualifier {block_qualifier!r} does not match " @@ -357,8 +357,7 @@ def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> ) if not isinstance(col_field, Integer): raise ValueError( - f"sum() column {col_name!r} is {type(col_field).__name__}, " - f"must be Integer in {expr!r}" + f"sum() column {col_name!r} is {type(col_field).__name__}, must be Integer in {expr!r}" ) @@ -395,12 +394,10 @@ def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> l operands = _names_in_expr(expr) for op in operands: if op not in known_dims and op not in derived_names: - raise ValueError( - f"derived_dims {name!r} operand {op!r} is not a known dimension" - ) + raise ValueError(f"derived_dims {name!r} operand {op!r} is not a known dimension") deps[name] = operands & derived_names - in_degree = {n: 0 for n in derived_names} + in_degree = dict.fromkeys(derived_names, 0) dependents: dict[str, set[str]] = {n: set() for n in derived_names} for name, dep_set in deps.items(): for dep in dep_set: @@ -436,8 +433,7 @@ class Block(BaseModel, Mapping): def _coerce_field_instances(cls, v: Any) -> Any: if isinstance(v, dict): return { - k: (val.model_dump() if isinstance(val, FieldBase) else val) - for k, val in v.items() + k: (val.model_dump() if isinstance(val, FieldBase) else val) for k, val in v.items() } return v @@ -454,10 +450,6 @@ def __len__(self) -> int: Blocks = Mapping[str, Block] -from packaging.version import Version -from pydantic import GetCoreSchemaHandler -from pydantic_core import core_schema - class _VersionPydanticAnnotation: @classmethod def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler): @@ -466,6 +458,7 @@ def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler): serialization=core_schema.to_string_ser_schema(), ) + VersionField = Annotated[Version, _VersionPydanticAnnotation] @@ -481,10 +474,12 @@ class ComponentBase(BaseModel): class Simulation(ComponentBase): type: Literal["simulation"] = "simulation" + class Model(ComponentBase): type: Literal["model"] = "model" solution: str | list[str] | None = None # compatible solution type(s) + class Package(ComponentBase): type: Literal["package"] = "package" multi: bool = False @@ -652,9 +647,7 @@ def _check_fields(fields: dict) -> None: f"{block_name!r} is not a list block in this component" ) item = list_field.item - item_fields: dict = ( - item.fields if isinstance(item, Record) else item.arms - ) + item_fields: dict = item.fields if isinstance(item, Record) else item.arms has_pk = any(getattr(f, "pk", False) for f in item_fields.values()) if not has_pk: raise ValueError( @@ -840,4 +833,4 @@ def load( components: dict[str, Component] = { name: MapV1To2.to_component(dfn) for name, dfn in dfns.items() } - return cls(components=components) \ No newline at end of file + return cls(components=components) From 1e8d3f97d87960b6e93d71a7a3cd287a0987ca9b Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 11 May 2026 14:19:32 -0700 Subject: [PATCH 03/29] spelling --- docs/md/dfn-schema.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/md/dfn-schema.md b/docs/md/dfn-schema.md index 145f7085..1d79c9c3 100644 --- a/docs/md/dfn-schema.md +++ b/docs/md/dfn-schema.md @@ -345,7 +345,7 @@ Type `integer`. ###### `time_series` -`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferrable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. +`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. ###### `pk` @@ -371,7 +371,7 @@ Type `double`. ###### `time_series` -`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferrable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. +`boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. #### Path From 3228ecc9dbb7954bda39734d035cb7dea0a5f325 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 11 May 2026 14:45:23 -0700 Subject: [PATCH 04/29] mypy + fixes --- autotest/test_dfns_registry.py | 24 +++--- autotest/test_dfns_schema.py | 32 ++++---- modflow_devtools/dfns/__init__.py | 127 +++++++++++++++++++++-------- modflow_devtools/dfns/__main__.py | 2 +- modflow_devtools/dfns/registry.py | 15 ++-- modflow_devtools/dfns/schema/v2.py | 64 ++------------- 6 files changed, 137 insertions(+), 127 deletions(-) diff --git a/autotest/test_dfns_registry.py b/autotest/test_dfns_registry.py index 7bc130d0..c30dd95b 100644 --- a/autotest/test_dfns_registry.py +++ b/autotest/test_dfns_registry.py @@ -93,25 +93,25 @@ def test_mapping_protocol(self, dfn_dir): # Test __len__ assert len(spec) > 100 # Should have many components - # Test __iter__ - names = list(spec) + # Test components iteration + names = list(spec.components) assert "sim-nam" in names assert "gwf-nam" in names assert "gwf-chd" in names - # Test __getitem__ - gwf_chd = spec["gwf-chd"] + # Test components access + gwf_chd = spec.components["gwf-chd"] assert gwf_chd.name == "gwf-chd" assert gwf_chd.parent == "gwf-nam" - # Test __contains__ - assert "gwf-chd" in spec - assert "nonexistent" not in spec + # Test components containment + assert "gwf-chd" in spec.components + assert "nonexistent" not in spec.components - # Test keys(), values(), items() - assert "gwf-wel" in spec.keys() - assert any(d.name == "gwf-wel" for d in spec.values()) - assert any(n == "gwf-wel" for n, d in spec.items()) + # Test components keys(), values(), items() + assert "gwf-wel" in spec.components.keys() + assert any(d.name == "gwf-wel" for d in spec.components.values()) + assert any(n == "gwf-wel" for n, d in spec.components.items()) def test_getitem_raises_keyerror(self, dfn_dir): """Test that __getitem__ raises KeyError for missing components.""" @@ -120,7 +120,7 @@ def test_getitem_raises_keyerror(self, dfn_dir): spec = DfnSpec.load(dfn_dir) with pytest.raises(KeyError, match="nonexistent"): - _ = spec["nonexistent"] + _ = spec.components["nonexistent"] def test_hierarchical_access(self, dfn_dir): """Test accessing components through the hierarchical tree.""" diff --git a/autotest/test_dfns_schema.py b/autotest/test_dfns_schema.py index 2ad216ad..80732c83 100644 --- a/autotest/test_dfns_schema.py +++ b/autotest/test_dfns_schema.py @@ -32,7 +32,7 @@ def _dim_block(*names: str) -> Block: """Build a dimensions Block with the named Integer dimension fields.""" return Block( name="dimensions", - fields={n: Integer(name=n, dimension=True) for n in names}, + fields={n: Integer(name=n, dimension="component") for n in names}, ) @@ -252,7 +252,7 @@ def test_dfnspec_construction_validates_dims(): derived_dims={"nodes": "nlay * nrow * ncol"}, ) spec = DfnSpec(components={"gwf-dis": pkg}) - assert "gwf-dis" in spec + assert "gwf-dis" in spec.components def test_dfnspec_construction_cycle_raises(): @@ -270,7 +270,7 @@ def test_dfnspec_construction_unknown_operand_raises(): def test_dfnspec_no_derived_dims_constructs_fine(): pkg = _pkg("gwf-chd", blocks=None, derived_dims=None) spec = DfnSpec(components={"gwf-chd": pkg}) - assert "gwf-chd" in spec + assert "gwf-chd" in spec.components # ── DfnSpec.explicit_dims_for ───────────────────────────────────────────────── @@ -359,29 +359,29 @@ def test_dfnspec_grid_dims_for_non_dis_siblings_excluded(): # ── DfnSpec Mapping protocol ────────────────────────────────────────────────── -def test_dfnspec_mapping_getitem(): +def test_dfnspec_components_getitem(): pkg = _pkg("gwf-chd", parent="gwf-nam") spec = DfnSpec(components={"gwf-chd": pkg}) - assert spec["gwf-chd"] is pkg + assert spec.components["gwf-chd"] is pkg -def test_dfnspec_mapping_iter(): +def test_dfnspec_components_iter(): pkg = _pkg("gwf-chd", parent="gwf-nam") spec = DfnSpec(components={"gwf-chd": pkg}) - assert list(spec) == ["gwf-chd"] + assert list(spec.components) == ["gwf-chd"] -def test_dfnspec_mapping_len(): +def test_dfnspec_components_len(): pkgs = {f"gwf-p{i}": _pkg(f"gwf-p{i}") for i in range(3)} spec = DfnSpec(components=pkgs) - assert len(spec) == 3 + assert len(spec.components) == 3 -def test_dfnspec_mapping_contains(): +def test_dfnspec_components_contains(): pkg = _pkg("gwf-chd") spec = DfnSpec(components={"gwf-chd": pkg}) - assert "gwf-chd" in spec - assert "gwf-rch" not in spec + assert "gwf-chd" in spec.components + assert "gwf-rch" not in spec.components # ── DfnSpec.schema_version ──────────────────────────────────────────────────── @@ -666,7 +666,7 @@ def test_dfnspec_valid_top_level_array_shape(): ) gwf = Model(name="gwf-nam", blocks=None) spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) - assert "gwf-dis" in spec + assert "gwf-dis" in spec.components def test_dfnspec_valid_array_in_record(): @@ -775,7 +775,7 @@ def _fk_pkg_and_spec(fk_val, pk_on_item=True, fk_ref=None): def test_validate_fk_fields_valid(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True) spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) - assert "gwf-lak" in spec + assert "gwf-lak" in spec.components def test_validate_fk_fields_unknown_block_raises(): @@ -793,7 +793,7 @@ def test_validate_fk_fields_no_pk_on_item_raises(): def test_validate_fk_fields_fk_ref_valid(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True, fk_ref="gwf-nam") spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) - assert "gwf-lak" in spec + assert "gwf-lak" in spec.components def test_validate_fk_fields_fk_ref_unknown_raises(): @@ -809,7 +809,7 @@ def test_validate_fk_fields_no_fk_set_passes(): pkg = Package(name="gwf-test", blocks={"data": block}) gwf = Model(name="gwf-nam", blocks=None) spec = DfnSpec(components={"gwf-nam": gwf, "gwf-test": pkg}) - assert "gwf-test" in spec + assert "gwf-test" in spec.components def test_validate_fk_fields_called_directly(): diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index 99e7be5a..cb428cb3 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -36,6 +36,7 @@ Array, Block, Blocks, + Component, DfnSpec, Double, FieldBase, @@ -69,6 +70,7 @@ "Array", "Block", "Blocks", + "Component", "Dfn", "DfnRegistry", "DfnRegistryDiscoveryError", @@ -343,19 +345,13 @@ def _map_field(f: FieldV1) -> FieldBase: preserve_case: bool = _to_bool(fd.get("preserve_case"), False) time_series: bool = _to_bool(fd.get("time_series"), False) valid = fd.get("valid") + _default_raw = fd.get("default") default = ( - try_literal_eval(fd.get("default")) if _type != "string" else fd.get("default") + try_literal_eval(_default_raw) + if _type != "string" and isinstance(_default_raw, str) + else _default_raw ) - common = { - "name": _name, - "longname": longname, - "description": description, - "optional": optional, - "default": default, - "developmode": developmode, - } - _COL_FK_RE = re.compile(r"^([A-Za-z_]\w*)\(([A-Za-z_]\w*)\)$") def _parse_shape(s: str) -> list[str]: @@ -414,10 +410,23 @@ def _parse_shape(s: str) -> list[str]: def _to_scalar() -> FieldBase: assert _type is not None if _type == "keyword": - return Keyword(**common, netcdf=netcdf) + return Keyword( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + ) if _type == "string": return String( - **common, + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, netcdf=netcdf, tagged=tagged, valid=list(valid) if valid else None, @@ -430,7 +439,9 @@ def _to_scalar() -> FieldBase: v = [int(x) for x in valid] if valid else None if fd.get("block") == "dimensions": if _name in GRID_DIM_NAMESPACE: - _dim_scope: str | None = "model" + _dim_scope: ( + Literal["record", "component", "model", "simulation"] | None + ) = "model" elif dfn.name == "sim-tdis" and _name == "nper": _dim_scope = "simulation" else: @@ -438,7 +449,12 @@ def _to_scalar() -> FieldBase: else: _dim_scope = None return Integer( - **common, + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, netcdf=netcdf, tagged=tagged, valid=v, @@ -446,7 +462,17 @@ def _to_scalar() -> FieldBase: dimension=_dim_scope, ) if _type in ("double", "double precision"): - return Double(**common, netcdf=netcdf, tagged=tagged, time_series=time_series) + return Double( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + tagged=tagged, + time_series=time_series, + ) raise TypeError(f"Unsupported scalar type: {_type!r}") def _row_field() -> "Record | Union": @@ -502,7 +528,7 @@ def _row_field() -> "Record | Union": description=( (description or "").replace("is the list of", "is the record of") or None ), - fields=children, + fields=children, # type: ignore[arg-type] ) def _union_fields() -> dict: @@ -533,18 +559,43 @@ def _record_fields() -> dict: if _type.startswith("recarray"): item = _row_field() - return List(item=item, **common, netcdf=netcdf) + return List( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + item=item, + ) if _type.startswith("keystring"): arms = _union_fields() - return Union(arms=arms, **common) + return Union( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + arms=arms, # type: ignore[arg-type] + ) if _type.startswith("record"): rec_fields = _record_fields() - return Record(fields=rec_fields, **common) + return Record( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + fields=rec_fields, # type: ignore[arg-type] + ) if shape_str is not None: - dtype_map = { + dtype_map: dict[str, Literal["keyword", "integer", "double", "string"]] = { "double precision": "double", "double": "double", "integer": "integer", @@ -558,7 +609,12 @@ def _record_fields() -> dict: # the v1 shape expression. dimension=True is set in a # second pass if another array references this field. return Array( - **common, + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, netcdf=netcdf, time_series=time_series, dtype="string", @@ -566,7 +622,12 @@ def _record_fields() -> dict: ) parsed_shape = _parse_shape(shape_str) return Array( - **common, + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, netcdf=netcdf, time_series=time_series, dtype=dtype, @@ -672,6 +733,7 @@ def _mark(fields: dict) -> dict: f = f.model_copy(update={"arms": _mark(f.arms)}) elif isinstance(f, List): item = f.item + new_item: Record | Union if isinstance(item, Record): local_dims = _record_local_dims(item) new_item_fields = _mark(item.fields) @@ -685,10 +747,8 @@ def _mark(fields: dict) -> dict: for fn, sf in new_item_fields.items() } new_item = item.model_copy(update={"fields": new_item_fields}) - elif isinstance(item, Union): - new_item = item.model_copy(update={"arms": _mark(item.arms)}) else: - new_item = item + new_item = item.model_copy(update={"arms": _mark(item.arms)}) f = f.model_copy(update={"item": new_item}) result[name] = f return result @@ -774,12 +834,11 @@ def _apply(fields: dict, block_name: str) -> dict: f = f.model_copy(update={"arms": _apply(f.arms, block_name)}) elif isinstance(f, List): item = f.item + new_item: Record | Union if isinstance(item, Record): new_item = _apply_record(item, block_name) - elif isinstance(item, Union): - new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) else: - new_item = item + new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) f = f.model_copy(update={"item": new_item}) result[name] = f return result @@ -850,7 +909,7 @@ def to_component(dfn: "Dfn") -> "Any": blocks = { block_name: Block( name=block_name, - fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, + fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, # type: ignore[misc] ) for block_name, block_fields in dfn.blocks.items() if isinstance(block_fields, dict) @@ -873,7 +932,9 @@ def to_component(dfn: "Dfn") -> "Any": if name.startswith("utl-"): return Package(**common, subtype="utility", multi=dfn.multi, variant_of=dfn.variant_of) has_period = bool(blocks and any("period" in k for k in blocks)) - subtype = "advanced" if dfn.advanced else "stress" if has_period else None + subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = ( + "advanced" if dfn.advanced else "stress" if has_period else None + ) return Package(**common, subtype=subtype, multi=dfn.multi, variant_of=dfn.variant_of) def map(self, dfn: "Dfn") -> "Dfn": @@ -1142,9 +1203,9 @@ def get_dfn( ref: str = "develop", source: str = "modflow6", path: str | PathLike | None = None, -) -> "Dfn": +) -> "Component": """ - Get a DFN by component name from the registry. + Get a component definition by name from the registry. """ registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) @@ -1175,4 +1236,4 @@ def list_components( """ registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) - return list(reg.spec.keys()) + return list(reg.spec.components.keys()) diff --git a/modflow_devtools/dfns/__main__.py b/modflow_devtools/dfns/__main__.py index bdfe4d78..5c18ad41 100644 --- a/modflow_devtools/dfns/__main__.py +++ b/modflow_devtools/dfns/__main__.py @@ -128,7 +128,7 @@ def cmd_list(args: argparse.Namespace) -> int: try: registry = get_registry(source=source, ref=ref, auto_sync=True) - components = list(registry.spec.keys()) + components = list(registry.spec.components.keys()) print(f"Components in {source}@{ref} ({len(components)} total):") for component in sorted(components): diff --git a/modflow_devtools/dfns/registry.py b/modflow_devtools/dfns/registry.py index 51a6dfc2..a19a6c0c 100644 --- a/modflow_devtools/dfns/registry.py +++ b/modflow_devtools/dfns/registry.py @@ -22,7 +22,8 @@ if TYPE_CHECKING: import pooch - from modflow_devtools.dfns import Dfn, DfnSpec + from modflow_devtools.dfns import DfnSpec + from modflow_devtools.dfns.schema.v2 import Component __all__ = [ "BootstrapConfig", @@ -311,13 +312,13 @@ def schema_version(self) -> Version: return self.spec.schema_version @property - def components(self) -> dict[str, Dfn]: + def components(self) -> dict[str, Component]: """Get all components as a flat dictionary.""" - return dict(self.spec.items()) + return dict(self.spec.components) - def get_dfn(self, component: str) -> Dfn: + def get_dfn(self, component: str) -> Component: """ - Get a DFN by component name. + Get a component definition by name. Parameters ---------- @@ -326,10 +327,10 @@ def get_dfn(self, component: str) -> Dfn: Returns ------- - Dfn + Component The requested component definition. """ - return self.spec[component] + return self.spec.components[component] def get_dfn_path(self, component: str) -> Path: """ diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema/v2.py index e6628b8a..619dfe2a 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema/v2.py @@ -1,6 +1,6 @@ import ast import re -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import Mapping from os import PathLike from typing import Annotated, Any, Literal @@ -21,10 +21,6 @@ class FieldBase(BaseModel): model_config = ConfigDict(frozen=True) - @property - def children(self) -> "dict[str, Field] | None": - return None - @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": type_ = d.get("type") @@ -166,7 +162,7 @@ def _validate_dimension(self) -> "Array": return self -class Record(FieldBase, Mapping): +class Record(FieldBase): type: Literal["record"] = "record" name: str longname: str | None = None @@ -180,17 +176,8 @@ class Record(FieldBase, Mapping): def children(self) -> "dict[str, Field]": return self.fields # type: ignore[return-value] - def __getitem__(self, key: str) -> "Field": - return self.children[key] - - def __iter__(self) -> Iterator[str]: - return iter(self.children) - - def __len__(self) -> int: - return len(self.children) - -class Union(FieldBase, Mapping): +class Union(FieldBase): type: Literal["union"] = "union" name: str longname: str | None = None @@ -204,17 +191,8 @@ class Union(FieldBase, Mapping): def children(self) -> "dict[str, Field]": return self.arms # type: ignore[return-value] - def __getitem__(self, key: str) -> "Field": - return self.children[key] - - def __iter__(self) -> Iterator[str]: - return iter(self.children) - - def __len__(self) -> int: - return len(self.children) - -class List(FieldBase, Sequence): +class List(FieldBase): type: Literal["list"] = "list" name: str longname: str | None = None @@ -229,15 +207,6 @@ class List(FieldBase, Sequence): def children(self) -> "dict[str, Field]": return {"item": self.item} # type: ignore[return-value] - def __getitem__(self, key: str) -> "Field": - return self.children[key] - - def __iter__(self) -> Iterator[str]: - return iter(self.children) - - def __len__(self) -> int: - return len(self.children) - Field = Annotated[ Keyword | String | Integer | Double | Path | Array | Record | Union | List, @@ -421,7 +390,7 @@ def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> l return order -class Block(BaseModel, Mapping): +class Block(BaseModel): model_config = ConfigDict(frozen=True) name: str fields: dict[str, Field] @@ -437,15 +406,6 @@ def _coerce_field_instances(cls, v: Any) -> Any: } return v - def __getitem__(self, key: str) -> Field: - return self.fields[key] - - def __iter__(self) -> Iterator[str]: - return iter(self.fields) - - def __len__(self) -> int: - return len(self.fields) - Blocks = Mapping[str, Block] @@ -557,7 +517,6 @@ def _validate_shape_element( f"does not resolve to a known dim " f"(explicit, derived, or grid)" ) - return if m := _LOOKUP_RE.fullmatch(element): block_name, col_name, fk_field_name = m.groups() @@ -716,21 +675,10 @@ def _check_array(arr: "Array", enclosing: "Record | None") -> None: _check_array(subfield, item) -class DfnSpec(BaseModel, Mapping): +class DfnSpec(BaseModel): model_config = ConfigDict(frozen=True) components: dict[str, Component] - # ── Mapping protocol ───────────────────────────────────────────────────── - - def __getitem__(self, key: str) -> Component: - return self.components[key] - - def __iter__(self) -> Iterator[str]: - return iter(self.components) - - def __len__(self) -> int: - return len(self.components) - # ── Properties ─────────────────────────────────────────────────────────── @property From e6e52c81da06476002c77fc6fcd7a68960a4eb23 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 10:23:35 -0700 Subject: [PATCH 05/29] revisions --- autotest/test_dfns.py | 9 ++- docs/md/dev/dfns.md | 119 ------------------------------ docs/md/dfn-schema.md | 20 ++--- docs/md/dfns.md | 30 +------- modflow_devtools/dfns/__init__.py | 23 +++++- modflow_devtools/dfns/parse.py | 31 +++++++- 6 files changed, 67 insertions(+), 165 deletions(-) diff --git a/autotest/test_dfns.py b/autotest/test_dfns.py index a80b577d..010ab637 100644 --- a/autotest/test_dfns.py +++ b/autotest/test_dfns.py @@ -95,11 +95,16 @@ def test_convert(function_tmpdir): assert gwf_data["parent"] == "sim-nam" assert gwf_data["schema_version"] == "2" + _COMPONENT_TYPES = {"simulation", "model", "package"} dfns = load_flat(function_tmpdir) roots = [] for dfn in dfns.values(): - if dfn.parent: - assert dfn.parent in dfns + parent = dfn.parent + if parent: + if isinstance(parent, list): + assert all(t in _COMPONENT_TYPES for t in parent) + else: + assert parent in dfns or parent in _COMPONENT_TYPES else: roots.append(dfn.name) assert len(roots) == 1 diff --git a/docs/md/dev/dfns.md b/docs/md/dev/dfns.md index f300e0d2..4b964dfd 100644 --- a/docs/md/dev/dfns.md +++ b/docs/md/dev/dfns.md @@ -722,93 +722,6 @@ The v2 schema should treat these as **separate layers**, where consumers can sel - Allow explicit schema version selection via API - Maintain backwards compatibility during transitions -### Tentative v2 schema design - -Based on feedback from mwtoews in [PR #229](https://github.com/MODFLOW-ORG/modflow-devtools/pull/229) and the structural/format separation discussed in [pyphoenix-project #246](https://github.com/modflowpy/pyphoenix-project/issues/246): - -**Structural vs format separation**: -The v2 schema should cleanly separate: -- **Structural specification**: Component definitions, relationships, variable data models - - Generated classes encode only structure and data models - - Use semantically meaningful dimensions (grid dimensions, time periods) -- **Format specification**: How MF6 reads/writes the data (separate layer) - - I/O layers exclusively handle input format concerns - - FILEIN/FILEOUT keywords, array input methods, etc. - -**Consolidated attributes**: Replace individual boolean fields with an `attrs` list: -```toml -# Instead of this (v1/v1.1): -optional = true -time_series = true -layered = false - -# Use this (v2): -attrs = ["optional", "time_series"] -``` - -**Array syntax for shapes**: Use actual arrays instead of string representations: -```toml -# Instead of this (v1/v1.1): -shape = "(nper, nnodes)" - -# Use this (v2): -shape = ["nper", "nnodes"] -``` - -**Format considerations**: -- **TOML vs YAML**: YAML's more forgiving whitespace better accommodates long descriptions (common for scientific parameters) -- **Validation approach**: Use Pydantic for both schema definition and validation - - Pydantic provides rigorous validation (addresses pyphoenix-project #246 requirement for formal specification) - - Built-in validation after parsing TOML/YAML to dict (no custom parsing logic) - - Automatic JSON-Schema generation for documentation and external tooling - - More Pythonic than using `python-jsonschema` directly - -**Pydantic integration**: -```python -from pydantic import BaseModel, Field -from typing import Any - -class FieldV2(BaseModel): - name: str - type: str - block: str | None = None - shape: list[str] | None = None - attrs: list[str] = Field(default_factory=list) - description: str = "" - default: Any = None - children: dict[str, "FieldV2"] | None = None - -# Usage: -# 1. Parse TOML/YAML to dict (using tomli/pyyaml/etc) -# 2. Validate with Pydantic (built-in) -parsed = tomli.load(f) -field = FieldV2(**parsed) # Validates automatically - -# 3. Export JSON-Schema if needed (for docs, external tools) -schema = FieldV2.model_json_schema() -``` - -Benefits: -- **Validation and schema in one**: Pydantic handles both, no separate validation library needed -- **Type safety**: Full Python type hints and IDE support -- **JSON-Schema export**: Available for documentation and external tooling -- **Widely adopted**: Well-maintained, used throughout Python ecosystem -- **Better UX**: Clear error messages, better handling of multi-line descriptions (if using YAML) - -## Component Hierarchy - -Component parent-child relationships are inferred from naming conventions by `to_tree()`. No separate specification file is required. - -**Current inference rules** (in `to_tree()`): -- `sim-nam` has no parent (root) -- `*-nam` components (e.g. `gwf-nam`, `gwt-nam`) are children of `sim-nam` -- `exg-*`, `sln-*`, `utl-*` components are children of `sim-nam` -- All other `-` components (e.g. `gwf-chd`) are children of `-nam` - -This inference is applied during `DfnSpec.load()` regardless of whether the underlying DFN files are legacy `.dfn` format or TOML. For v2 TOML files, `parent` attributes in individual component files are respected when present and take precedence over inference. - -**Planned for v2**: Explicit parent-child relationships via `parent` attributes in per-component TOML files, eliminating reliance on naming conventions. The `to_tree()` inference will remain as a fallback for v1/v1.1 compatibility. - ## Schema version support The DFNs API will support **multiple schema versions simultaneously**: @@ -1005,38 +918,6 @@ The DFNs API follows the same design patterns as the Models and Programs APIs fo - Schema versioning and mapping capabilities - No `MergedRegistry` (users work with one MF6 version at a time) -## Design Decisions - -### Use Pooch for fetching - -Following the recommendation in [issue #262](https://github.com/MODFLOW-ORG/modflow-devtools/issues/262), the DFNs API will use Pooch for fetching to avoid maintaining custom HTTP client code. This provides: - -- **Automatic caching**: Pooch handles local caching with verification -- **Hash verification**: Ensures file integrity -- **Progress bars**: Better user experience for downloads -- **Well-tested**: Pooch is mature and widely used -- **Consistency**: Same approach as Models API - -### Use Pydantic for schema validation - -Pydantic will be used for defining and validating DFN schemas (both registry schemas and DFN content schemas): - -- **Built-in validation**: No need for separate validation libraries like `python-jsonschema` -- **Type safety**: Full Python type hints and IDE support -- **JSON-Schema export**: Can generate JSON-Schema for documentation and external tooling -- **Developer experience**: Clear error messages, good Python integration -- **Justification**: Widely adopted, well-maintained, addresses the formal specification requirement from [pyphoenix-project #246](https://github.com/modflowpy/pyphoenix-project/issues/246) - -### Schema versioning strategy - -Based on [issue #259](https://github.com/MODFLOW-ORG/modflow-devtools/issues/259): - -- **Separate format from schema**: Registry metadata includes both -- **Support v1.1 as mainline**: Don't jump straight to v2 -- **Backwards compatible**: Continue supporting v1 for existing MODFLOW 6 releases -- **Schema mapping**: Provide transparent conversion via `map()` function -- **Future-proof**: Design allows for v2 when ready (devtools 2.x / FloPy 4.x) - ### Future enhancements 1. **Release asset mode**: Add support for registries as release assets (in addition to version control) diff --git a/docs/md/dfn-schema.md b/docs/md/dfn-schema.md index 1d79c9c3..4372dc2e 100644 --- a/docs/md/dfn-schema.md +++ b/docs/md/dfn-schema.md @@ -100,6 +100,7 @@ Component definitions consist primarily of a name, zero or more block definition - `blocks`: block definitions - `parent`: parent component(s) - `schema_version`: DFN schema version +- `derived_dims`: dimensions computed from other dimensions Components may refer to, i.e. be constrained by, other components. Cross-component constraints include parent-child relations, solution compatibility, and format variants. @@ -165,7 +166,7 @@ A package is any component that is not a simulation or a model. ###### `multi` -`boolean (default: false)`. Indicates that multiple instances of this component are permitted. Components of which multiple instances are allowed are called "multi-packages". +`boolean (default: false)`. Indicates that multiple instances of this component may be attached to the same parent component. Components of which multiple instances are allowed are called "multi-packages". ###### `subtype` @@ -217,9 +218,7 @@ A field's value need not be preceded by its name; see the `tagged` section below `boolean (default: false)`. Whether the block may appear multiple times in an input file. When true, each occurrence is read independently, and associated with a unique label. The canonical repeating block is the period block, whose label is the stress period number. -#### `optional` - -`boolean (default: false)`. Whether the block may be omitted entirely from the input file. An absent optional block is treated as empty. +**Note:** if a repeating block contains any required fields, it must appear at least once. If a repeating block contains only optional fields, it can appear zero or more times. ## Fields @@ -303,7 +302,7 @@ Type `string`. ###### `tagged` -`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. ###### `valid` @@ -333,7 +332,7 @@ Type `integer`. ###### `tagged` -`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. ###### `valid` @@ -367,7 +366,7 @@ Type `double`. ###### `tagged` -`boolean (default: false)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. ###### `time_series` @@ -381,7 +380,7 @@ Type `path`. ###### `mode` -`"filein" | "fileout"`. Whether the path is to an input or output file. +`"filein" | "fileout"`. Whether the path is to an input or output file. Required. ### Composites @@ -466,7 +465,7 @@ Shape expressions for non-string arrays may use one of three structural forms. A - **Intra-record sibling reference**: a dim reference that names a sibling `integer` or `dimension: true` `array` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. - **Row-level column lookup** (`block.column(fk_field)`): a cross-list per-row quantity, valid only for array subfields of records. See below. -Any dim reference (either of the first two forms) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`), e.g. ``, `<=`, or `>=`). The dim portion validates normally; the bound is advisory and is not enforced by the MF6 parser. A shape expression that does not match one of these forms is a schema validation error. String arrays (`dtype: "string"`) must have empty `shape`. @@ -514,7 +513,8 @@ This notation is consistent with FK path conventions (`block.field` for within-c Validation rules: - `fk_field` must be a sibling field in the same enclosing record -- `fk_field`'s `fk` attribute block portion must match `block` +- `fk_field` must have `fk` set, and its `fk` attribute's block portion must match `block` +- `block`'s list item record must have exactly one `pk: true` field - `column` must exist in `block`'s item record and be of type `integer` - This form is only valid when the array is a subfield of a record; it is a schema error on a top-level array field diff --git a/docs/md/dfns.md b/docs/md/dfns.md index a7434e7c..e10c1d9b 100644 --- a/docs/md/dfns.md +++ b/docs/md/dfns.md @@ -4,10 +4,8 @@ MODFLOW 6 specifies input components and their variables in configuration files `modflow_devtools` provides two modules for working with MODFLOW 6 input specification files: -- **`modflow_devtools.dfn`** — stable module, available in all current releases -- **`modflow_devtools.dfns`** — experimental new API, subject to change without notice - ---- +- **`modflow_devtools.dfn`:** stable, soon-to-be deprecated +- **`modflow_devtools.dfns`:** experimental, subject to change without notice ## `modflow_devtools.dfn` (stable) @@ -23,30 +21,6 @@ get_dfns("MODFLOW-ORG", "modflow6", "6.6.0", "/tmp/dfns") Downloads all `.dfn` files for the specified MODFLOW 6 release into the given output directory (returns `None`). -### Types - -The core types are `TypedDict`s: - -```python -from modflow_devtools.dfn import Dfn, Field - -# Dfn: top-level component (e.g. "gwf-chd") -# name: str -# advanced: bool -# multi: bool -# : dict[str, Field] (one key per block, e.g. "options", "period") - -# Field: individual input variable within a block -# name: str -# type: str (e.g. "keyword", "integer", "double precision", "string", ...) -# block: str -# shape: str | None (e.g. "(naux)") -# default: Any -# children: dict[str, Field] | None -# description: str | None -# reader: str (e.g. "urword") -``` - ### Converting to TOML The `dfn` dependency group is required for the TOML conversion tool: diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index cb428cb3..89d1aa22 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -758,7 +758,7 @@ def _mark(fields: dict) -> dict: @staticmethod def _infer_fk_from_shapes(blocks: "dict[str, dict]") -> "dict[str, dict]": """ - Fourth pass: infer fk= and pk= from resolved lookup shape elements. + Post-pass 3: infer fk= and pk= from resolved lookup shape elements. When _parse_shape resolves a v1 shorthand like "col(fk_field)" to the canonical form "block.col(fk_field)", the fk_field sibling in the same @@ -1077,6 +1077,21 @@ def _infer_parent(name: str) -> "str | None": return None +def _resolve_parent_for_tree(name: str, parent: "str | list[str] | None", dfns: Dfns) -> "str | None": + """ + Resolve a parent value to a specific component name for tree placement. + + When parent is a type label (e.g. "model", ["model", "package"]) or any + string not present in the known component dict, falls back to name-based + inference so the DFN is still placed in the tree. + """ + if parent is None: + return None + if isinstance(parent, str) and parent in dfns: + return parent + return _infer_parent(name) + + def _apply_parent_inference(dfns: Dfns) -> Dfns: """Set parent on any Dfn where it is not already explicit.""" result = {} @@ -1109,7 +1124,11 @@ def to_tree(dfns: Dfns) -> Dfn: def _build_tree(node_name: str) -> Dfn: node = dfns[node_name] - children = {name: dfn for name, dfn in dfns.items() if dfn.parent == node_name} + children = { + name: dfn + for name, dfn in dfns.items() + if _resolve_parent_for_tree(name, dfn.parent, dfns) == node_name + } if children: node = node.model_copy( update={"children": {name: _build_tree(name) for name in children}} diff --git a/modflow_devtools/dfns/parse.py b/modflow_devtools/dfns/parse.py index de68a906..ca25219b 100644 --- a/modflow_devtools/dfns/parse.py +++ b/modflow_devtools/dfns/parse.py @@ -56,10 +56,22 @@ def try_parse_bool(value: Any) -> Any: return value -def try_parse_parent(meta: list[str]) -> str | None: +_FLOPY_CLASS_TO_V2_TYPE: dict[str, str] = { + "MFSimulation": "simulation", + "MFModel": "model", + "MFPackage": "package", +} + + +def try_parse_parent(meta: list[str]) -> "str | list[str] | None": """ - Try to parse a component's parent component name from its metadata. - Return `None` if it has no parent specified. + Try to parse a component's parent from its metadata. + + Returns a v2 component type label (e.g. "model", "package", + ["model", "package"]) when the metadata uses the flopy + ``parent_name_type `` format, + a specific component name when a legacy ``parent `` line + is present, or ``None`` if no parent is declared. """ line = next( iter(m for m in meta if isinstance(m, str) and m.startswith("parent")), @@ -68,7 +80,18 @@ def try_parse_parent(meta: list[str]) -> str | None: if not line: return None split = line.split() - return split[1] + if not split: + return None + # "parent_name_type " — flopy class names + # map to v2 component type labels. + if split[0] == "parent_name_type" and len(split) >= 3: + classes = split[2].split("/") + types = [_FLOPY_CLASS_TO_V2_TYPE[c] for c in classes if c in _FLOPY_CLASS_TO_V2_TYPE] + if types: + return types[0] if len(types) == 1 else types + return None + # Legacy: "parent " + return split[1] if len(split) >= 2 else None def is_advanced_package(meta: list[str]) -> bool: From 05c55ade390b15af7de4b18d96b324632cda362b Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 19:59:49 -0700 Subject: [PATCH 06/29] clean v1/v1.1/v2 split --- autotest/test_dfn.py | 50 + autotest/test_dfns.py | 536 ++++++++--- modflow_devtools/dfn/__init__.py | 34 + modflow_devtools/dfn/mapper.py | 346 +++++++ modflow_devtools/{dfns => dfn}/parse.py | 0 modflow_devtools/{dfn.py => dfn/v1.py} | 0 modflow_devtools/dfn/v1_1.py | 195 ++++ modflow_devtools/dfn2toml.py | 133 ++- modflow_devtools/dfns/__init__.py | 1133 +---------------------- modflow_devtools/dfns/dfn2toml.py | 121 --- modflow_devtools/dfns/mapper.py | 693 ++++++++++++++ modflow_devtools/dfns/schema/v1.py | 59 -- modflow_devtools/dfns/schema/v2.py | 20 +- 13 files changed, 1875 insertions(+), 1445 deletions(-) create mode 100644 modflow_devtools/dfn/__init__.py create mode 100644 modflow_devtools/dfn/mapper.py rename modflow_devtools/{dfns => dfn}/parse.py (100%) rename modflow_devtools/{dfn.py => dfn/v1.py} (100%) create mode 100644 modflow_devtools/dfn/v1_1.py delete mode 100644 modflow_devtools/dfns/dfn2toml.py create mode 100644 modflow_devtools/dfns/mapper.py delete mode 100644 modflow_devtools/dfns/schema/v1.py diff --git a/autotest/test_dfn.py b/autotest/test_dfn.py index 72c58d66..f32ef9d6 100644 --- a/autotest/test_dfn.py +++ b/autotest/test_dfn.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from packaging.version import Version from modflow_devtools.dfn import Dfn, get_dfns from modflow_devtools.dfn2toml import convert @@ -9,6 +10,8 @@ PROJ_ROOT = Path(__file__).parents[1] DFN_DIR = PROJ_ROOT / "autotest" / "temp" / "dfn" TOML_DIR = DFN_DIR / "toml" +TOML_V1_1_DIR = DFN_DIR / "toml-v1_1" +TOML_V2_DIR = DFN_DIR / "toml-v2" VERSIONS = {1: DFN_DIR, 2: TOML_DIR} MF6_OWNER = "MODFLOW-ORG" MF6_REPO = "modflow6" @@ -36,6 +39,26 @@ def pytest_generate_tests(metafunc): toml_names = [toml.stem for toml in TOML_DIR.glob("*.toml")] metafunc.parametrize("toml_name", toml_names, ids=toml_names) + if "toml_v1_1_name" in metafunc.fixturenames: + dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] + if not TOML_V1_1_DIR.exists() or not all( + (TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths + ): + convert(DFN_DIR, TOML_V1_1_DIR, schema="1.1") + assert all((TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) + toml_names = [toml.stem for toml in TOML_V1_1_DIR.glob("*.toml")] + metafunc.parametrize("toml_v1_1_name", toml_names, ids=toml_names) + + if "toml_v2_name" in metafunc.fixturenames: + dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] + if not TOML_V2_DIR.exists() or not all( + (TOML_V2_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths + ): + convert(DFN_DIR, TOML_V2_DIR, schema="2") + assert all((TOML_V2_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) + toml_names = [toml.stem for toml in TOML_V2_DIR.glob("*.toml")] + metafunc.parametrize("toml_v2_name", toml_names, ids=toml_names) + @requires_pkg("boltons") def test_load_v1(dfn_name): @@ -60,3 +83,30 @@ def test_load_v2(toml_name): def test_load_all(version): dfns = Dfn.load_all(VERSIONS[version], version=version) assert any(dfns) + + +@requires_pkg("boltons") +def test_convert_v1_1(toml_v1_1_name): + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] + + with (TOML_V1_1_DIR / f"{toml_v1_1_name}.toml").open("rb") as f: + data = tomllib.load(f) + assert data["name"] == toml_v1_1_name + assert data["schema_version"] == "1.1" + + +@requires_pkg("boltons") +def test_convert_v2(toml_v2_name): + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] + from pydantic import TypeAdapter + from modflow_devtools.dfns.schema.v2 import Component + + with (TOML_V2_DIR / f"{toml_v2_name}.toml").open("rb") as f: + data = tomllib.load(f) + assert TypeAdapter(Component).validate_python(data).name == toml_v2_name diff --git a/autotest/test_dfns.py b/autotest/test_dfns.py index 010ab637..e7874841 100644 --- a/autotest/test_dfns.py +++ b/autotest/test_dfns.py @@ -1,12 +1,25 @@ +import dataclasses from pathlib import Path import pytest from packaging.version import Version -from modflow_devtools.dfns import Dfn, _load_common, load, load_flat -from modflow_devtools.dfns.dfn2toml import convert, is_valid +from modflow_devtools.dfn.mapper import ( + _apply_parent_inference, + _dfn_to_plain_dict, + _load_common, + _toml_safe, + load, + load_flat, + map as map_v1_1, + to_flat, + to_tree, +) +from modflow_devtools.dfn.v1_1 import Dfn, FieldV1, FieldV1_1 +from modflow_devtools.dfns import is_valid from modflow_devtools.dfns.fetch import fetch_dfns -from modflow_devtools.dfns.schema.v1 import FieldV1 +from modflow_devtools.dfns.mapper import MapV1To2 +from modflow_devtools.dfns.mapper import map as map_v2 from modflow_devtools.dfns.schema.v2 import ( Array, Double, @@ -20,8 +33,6 @@ PROJ_ROOT = Path(__file__).parents[1] DFN_DIR = PROJ_ROOT / "autotest" / "temp" / "dfns" -TOML_DIR = DFN_DIR / "toml" -SPEC_DIRS = {1: DFN_DIR, 2: TOML_DIR} MF6_OWNER = "MODFLOW-ORG" MF6_REPO = "modflow6" MF6_REF = "develop" @@ -37,16 +48,6 @@ def pytest_generate_tests(metafunc): ] metafunc.parametrize("dfn_name", dfn_names, ids=dfn_names) - if "toml_name" in metafunc.fixturenames: - dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] - if not TOML_DIR.exists() or not all( - (TOML_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths - ): - convert(DFN_DIR, TOML_DIR) - assert all((TOML_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) - toml_names = [toml.stem for toml in TOML_DIR.glob("*.toml")] - metafunc.parametrize("toml_name", toml_names, ids=toml_names) - @requires_pkg("boltons") def test_load_v1(dfn_name): @@ -60,73 +61,12 @@ def test_load_v1(dfn_name): @requires_pkg("boltons") -def test_load_v2(toml_name): - with (TOML_DIR / f"{toml_name}.toml").open(mode="rb") as toml_file: - dfn = load(toml_file, name=toml_name, format="toml") - assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) - - -@requires_pkg("boltons") -@pytest.mark.parametrize("schema_version", list(SPEC_DIRS.keys())) -def test_load_all(schema_version): - dfns = load_flat(path=SPEC_DIRS[schema_version]) +def test_load_all(): + dfns = load_flat(path=DFN_DIR) for dfn in dfns.values(): assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) -@requires_pkg("boltons", "tomli") -def test_convert(function_tmpdir): - import tomli - - convert(DFN_DIR, function_tmpdir) - - assert (function_tmpdir / "sim-nam.toml").exists() - assert (function_tmpdir / "gwf-nam.toml").exists() - - with (function_tmpdir / "sim-nam.toml").open("rb") as f: - sim_data = tomli.load(f) - assert sim_data["name"] == "sim-nam" - assert sim_data["schema_version"] == "2" - assert "parent" not in sim_data - - with (function_tmpdir / "gwf-nam.toml").open("rb") as f: - gwf_data = tomli.load(f) - assert gwf_data["name"] == "gwf-nam" - assert gwf_data["parent"] == "sim-nam" - assert gwf_data["schema_version"] == "2" - - _COMPONENT_TYPES = {"simulation", "model", "package"} - dfns = load_flat(function_tmpdir) - roots = [] - for dfn in dfns.values(): - parent = dfn.parent - if parent: - if isinstance(parent, list): - assert all(t in _COMPONENT_TYPES for t in parent) - else: - assert parent in dfns or parent in _COMPONENT_TYPES - else: - roots.append(dfn.name) - assert len(roots) == 1 - root = dfns[roots[0]] - assert root.name == "sim-nam" - - models = root.children or {} - for mdl in models: - assert models[mdl].name == mdl - assert models[mdl].parent == "sim-nam" - - if gwf := models.get("gwf-nam", None): - pkgs = gwf.children or {} - pkgs = {k: v for k, v in pkgs.items() if k.startswith("gwf-")} - assert len(pkgs) > 0 - if dis := pkgs.get("gwf-dis", None): - assert dis.name == "gwf-dis" - assert dis.parent == "gwf-nam" - assert "options" in (dis.blocks or {}) - assert "dimensions" in (dis.blocks or {}) - - def test_dfn_from_dict_ignores_extra_keys(): d = { "schema_version": Version("2"), @@ -176,7 +116,7 @@ def test_dfn_from_dict_roundtrip(): multi=True, blocks={"options": {}}, ) - d = original.model_dump() + d = dataclasses.asdict(original) reconstructed = Dfn.from_dict(d) assert reconstructed.name == original.name assert reconstructed.schema_version == original.schema_version @@ -209,8 +149,6 @@ def test_fieldv1_from_dict_strict_mode(): def test_fieldv1_from_dict_roundtrip(): - from dataclasses import asdict - original = FieldV1( name="maxbound", type="integer", @@ -218,7 +156,7 @@ def test_fieldv1_from_dict_roundtrip(): description="maximum number of cells", tagged=True, ) - d = asdict(original) + d = dataclasses.asdict(original) reconstructed = FieldV1.from_dict(d) assert reconstructed.name == original.name assert reconstructed.type == original.type @@ -395,9 +333,7 @@ def test_validate_nonexistent_file(function_tmpdir): def test_fieldv1_to_fieldv2_conversion(): - """Test that FieldV1 instances are properly converted to typed v2 instances.""" - from modflow_devtools.dfns import map - + """Test that FieldV1 instances are properly converted to typed v2 Component fields.""" dfn_v1 = Dfn( schema_version=Version("1"), name="test-dfn", @@ -422,13 +358,15 @@ def test_fieldv1_to_fieldv2_conversion(): }, ) - dfn_v2 = map(dfn_v1, schema_version="2") - assert dfn_v2.schema_version == Version("2") - assert dfn_v2.blocks is not None - assert "options" in dfn_v2.blocks - assert "save_flows" in dfn_v2.blocks["options"] + component = map_v2(dfn_v1, schema_version="2") + assert component.schema_version == Version("2") + assert component.blocks is not None + assert "options" in component.blocks + + options = component.blocks["options"].fields + assert "save_flows" in options - save_flows = dfn_v2.blocks["options"]["save_flows"] + save_flows = options["save_flows"] assert isinstance(save_flows, Keyword) assert isinstance(save_flows, FieldBase) assert save_flows.name == "save_flows" @@ -437,7 +375,7 @@ def test_fieldv1_to_fieldv2_conversion(): assert not hasattr(save_flows, "in_record") assert not hasattr(save_flows, "reader") - some_float = dfn_v2.blocks["options"]["some_float"] + some_float = options["some_float"] assert isinstance(some_float, Double) assert some_float.name == "some_float" assert some_float.type == "double" @@ -446,8 +384,6 @@ def test_fieldv1_to_fieldv2_conversion(): def test_fieldv1_to_fieldv2_conversion_with_children(): """Test that FieldV1 with nested children are properly converted to typed v2 instances.""" - from modflow_devtools.dfns import map - dfn_v1 = Dfn( schema_version=Version("1"), name="test-dfn", @@ -472,10 +408,10 @@ def test_fieldv1_to_fieldv2_conversion_with_children(): }, ) - dfn_v2 = map(dfn_v1, schema_version="2") - assert dfn_v2.blocks is not None - for block_fields in dfn_v2.blocks.values(): - for f in block_fields.values(): + component = map_v2(dfn_v1, schema_version="2") + assert component.blocks is not None + for block in component.blocks.values(): + for f in block.fields.values(): assert isinstance(f, FieldBase) if f.children: for child in f.children.values(): @@ -484,8 +420,6 @@ def test_fieldv1_to_fieldv2_conversion_with_children(): def test_period_block_conversion(): """Test period block recarray conversion to individual arrays.""" - from modflow_devtools.dfns import map - dfn_v1 = Dfn( schema_version=Version("1"), name="test-pkg", @@ -515,12 +449,12 @@ def test_period_block_conversion(): }, ) - dfn_v2 = map(dfn_v1, schema_version="2") + component = map_v2(dfn_v1, schema_version="2") - period_block = dfn_v2.blocks["period"] - assert "cellid" not in period_block - assert "q" in period_block - q = period_block["q"] + period_fields = component.blocks["period"].fields + assert "cellid" not in period_fields + assert "q" in period_fields + q = period_fields["q"] assert isinstance(q, Array) assert "nper" in q.shape assert "nodes" in q.shape @@ -529,8 +463,6 @@ def test_period_block_conversion(): def test_record_type_conversion(): """Test record type with multiple scalar fields.""" - from modflow_devtools.dfns import map - dfn_v1 = Dfn( schema_version=Version("1"), name="test-dfn", @@ -558,9 +490,9 @@ def test_record_type_conversion(): }, ) - dfn_v2 = map(dfn_v1, schema_version="2") + component = map_v2(dfn_v1, schema_version="2") - auxrecord = dfn_v2.blocks["options"]["auxrecord"] + auxrecord = component.blocks["options"].fields["auxrecord"] assert isinstance(auxrecord, Record) assert auxrecord.type == "record" assert auxrecord.children is not None @@ -572,8 +504,6 @@ def test_record_type_conversion(): def test_keystring_type_conversion(): """Test keystring (union) type conversion.""" - from modflow_devtools.dfns import map - dfn_v1 = Dfn( schema_version=Version("1"), name="test-dfn", @@ -608,10 +538,390 @@ def test_keystring_type_conversion(): }, ) - dfn_v2 = map(dfn_v1, schema_version="2") + component = map_v2(dfn_v1, schema_version="2") - obs_rec = dfn_v2.blocks["options"]["obs_filerecord"] + obs_rec = component.blocks["options"].fields["obs_filerecord"] assert isinstance(obs_rec, Record) assert obs_rec.type == "record" assert obs_rec.children is not None assert all(isinstance(child, FieldBase) for child in obs_rec.children.values()) + + +# ============================================================================= +# Group 1: MapV1To1_1 +# ============================================================================= + + +def test_mapv1to1_1_field_stripping(): + """map(dfn, '1.1') strips v1-specific attrs; shared base attrs are preserved.""" + dfn_v1 = Dfn( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "save_flows": FieldV1( + name="save_flows", + type="keyword", + block="options", + description="save calculated flows", + tagged=True, + in_record=False, + reader="urword", + ), + } + }, + ) + + dfn_v1_1 = map_v1_1(dfn_v1, "1.1") + assert dfn_v1_1.schema_version == Version("1.1") + assert dfn_v1_1.blocks is not None + + f = dfn_v1_1.blocks["options"]["save_flows"] + assert isinstance(f, FieldV1_1) + assert not isinstance(f, FieldV1) + assert f.name == "save_flows" + assert f.type == "keyword" + assert f.description == "save calculated flows" + assert f.tagged is True + assert not hasattr(f, "in_record") + assert not hasattr(f, "reader") + + +def test_mapv1to1_1_preserves_dfn_metadata(): + """map(dfn, '1.1') preserves DFN-level metadata (name, parent, advanced, multi).""" + dfn_v1 = Dfn( + schema_version=Version("1"), + name="gwf-chd", + parent="gwf-nam", + advanced=False, + multi=True, + blocks={}, + ) + + dfn_v1_1 = map_v1_1(dfn_v1, "1.1") + assert dfn_v1_1.name == "gwf-chd" + assert dfn_v1_1.parent == "gwf-nam" + assert dfn_v1_1.advanced is False + assert dfn_v1_1.multi is True + + +def test_block_sort_key_order(): + """block_sort_key orders blocks in canonical MF6 order.""" + from modflow_devtools.dfn.v1_1 import block_sort_key + + items = [ + ("period", {}), + ("options", {}), + ("packagedata", {}), + ("dimensions", {}), + ("custom_block", {}), + ] + sorted_items = sorted(items, key=block_sort_key) + assert [k for k, _ in sorted_items] == [ + "options", + "dimensions", + "packagedata", + "period", + "custom_block", + ] + + +# ============================================================================= +# Group 2: map() dispatch edge cases +# ============================================================================= + + +def test_map_dispatch_to_v1_raises(): + """map(dfn, '1') raises NotImplementedError.""" + dfn = Dfn(schema_version=Version("1"), name="test-dfn") + with pytest.raises(NotImplementedError): + map_v1_1(dfn, "1") + + +def test_map_dispatch_unsupported_version_raises(): + """map(dfn, unsupported version) raises ValueError.""" + dfn = Dfn(schema_version=Version("1"), name="test-dfn") + with pytest.raises(ValueError): + map_v1_1(dfn, "3") + + +def test_map_dispatch_already_v1_1_returns_same(): + """map(dfn, '1.1') when dfn is already v1.1 returns the same Dfn unchanged.""" + dfn = Dfn(schema_version=Version("1.1"), name="test-dfn") + result = map_v1_1(dfn, "1.1") + assert result is dfn + + +# ============================================================================= +# Group 3: to_tree / to_flat / _apply_parent_inference +# ============================================================================= + + +def test_apply_parent_inference(): + """_apply_parent_inference infers parents from component names.""" + dfns = { + "sim-nam": Dfn(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": Dfn(schema_version=Version("1.1"), name="gwf-nam"), + "gwf-dis": Dfn(schema_version=Version("1.1"), name="gwf-dis"), + } + inferred = _apply_parent_inference(dfns) + assert inferred["sim-nam"].parent is None + assert inferred["gwf-nam"].parent == "sim-nam" + assert inferred["gwf-dis"].parent == "gwf-nam" + + +def test_apply_parent_inference_does_not_overwrite_explicit(): + """_apply_parent_inference does not overwrite an already-set parent.""" + dfns = { + "gwf-dis": Dfn( + schema_version=Version("1.1"), name="gwf-dis", parent="custom-parent" + ), + } + inferred = _apply_parent_inference(dfns) + assert inferred["gwf-dis"].parent == "custom-parent" + + +def test_to_tree_builds_hierarchy(): + """to_tree() builds children hierarchy from a flat Dfns dict.""" + dfns = { + "sim-nam": Dfn(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": Dfn(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": Dfn(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + root = to_tree(dfns) + assert root.name == "sim-nam" + assert root.children is not None + assert "gwf-nam" in root.children + gwf_nam = root.children["gwf-nam"] + assert gwf_nam.children is not None + assert "gwf-dis" in gwf_nam.children + + +def test_to_flat_strips_children(): + """to_flat() recovers the flat spec; no node has children set.""" + dfns = { + "sim-nam": Dfn(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": Dfn(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": Dfn(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + root = to_tree(dfns) + flat = to_flat(root) + assert set(flat.keys()) == {"sim-nam", "gwf-nam", "gwf-dis"} + for dfn in flat.values(): + assert dfn.children is None + + +def test_to_tree_raises_without_unique_root(): + """to_tree() raises ValueError when there is no single root component.""" + dfns = { + "gwf-nam": Dfn(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": Dfn(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + with pytest.raises(ValueError, match="root"): + to_tree(dfns) + + +def test_to_tree_raises_for_v1_schema(): + """to_tree() raises NotImplementedError for v1 schema.""" + dfns = { + "sim-nam": Dfn(schema_version=Version("1"), name="sim-nam"), + } + with pytest.raises(NotImplementedError): + to_tree(dfns) + + +# ============================================================================= +# Group 4: to_component() branches +# ============================================================================= + + +def test_to_component_simulation(): + """sim-nam maps to Simulation.""" + from modflow_devtools.dfns.schema.v2 import Simulation + + dfn = Dfn(schema_version=Version("2"), name="sim-nam") + result = map_v2(dfn, "2") + assert isinstance(result, Simulation) + + +def test_to_component_model(): + """*-nam (non-sim) maps to Model.""" + from modflow_devtools.dfns.schema.v2 import Model + + dfn = Dfn(schema_version=Version("2"), name="gwf-nam") + result = map_v2(dfn, "2") + assert isinstance(result, Model) + + +def test_to_component_solution_package(): + """sln-* maps to Package(subtype='solution').""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="sln-ims") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "solution" + + +def test_to_component_exchange_package(): + """exg-* maps to Package(subtype='exchange').""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="exg-gwfgwf") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "exchange" + + +def test_to_component_utility_package(): + """utl-* maps to Package(subtype='utility').""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="utl-obs") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "utility" + + +def test_to_component_advanced_package(): + """advanced=True maps to Package(subtype='advanced').""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="gwf-sfr", advanced=True) + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "advanced" + + +def test_to_component_variant_of_g(): + """Names ending in 'g' infer variant_of to the name without the suffix.""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="gwf-welg") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.variant_of == "gwf-wel" + + +def test_to_component_variant_of_a(): + """Names ending in 'a' infer variant_of to the name without the suffix.""" + from modflow_devtools.dfns.schema.v2 import Package + + dfn = Dfn(schema_version=Version("2"), name="gwf-rcha") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.variant_of == "gwf-rch" + + +# ============================================================================= +# Group 5: MapV1To2.map() fast-path +# ============================================================================= + + +def test_mapv1to2_fastpath_skips_map_blocks(): + """map(dfn, '2') with schema_version=2 takes the fast path (no map_blocks call). + + If map_blocks were called, it would fail: FieldBase has no ``in_record`` attr, + and asdict() on a Pydantic model raises TypeError. + """ + dfn = Dfn( + schema_version=Version("2"), + name="gwf-chd", + blocks={ + "options": { + "save_flows": Keyword(name="save_flows", description="save flows"), + } + }, + ) + result = map_v2(dfn, "2") + assert result.name == "gwf-chd" + assert result.blocks is not None + assert "save_flows" in result.blocks["options"].fields + + +# ============================================================================= +# Group 6: _dfn_to_plain_dict / _toml_safe +# ============================================================================= + + +def test_dfn_to_plain_dict_version_coerced_and_none_excluded(): + """Version is coerced to str; None fields are excluded from output.""" + dfn = Dfn( + schema_version=Version("1.1"), + name="test-dfn", + parent=None, + blocks=None, + ) + d = _dfn_to_plain_dict(dfn) + assert d["schema_version"] == "1.1" + assert d["name"] == "test-dfn" + assert "parent" not in d + assert "blocks" not in d + + +def test_dfn_to_plain_dict_with_fieldbase_blocks(): + """FieldBase blocks are serialized via model_dump.""" + dfn = Dfn( + schema_version=Version("2"), + name="test-dfn", + blocks={ + "options": { + "nper": Integer(name="nper", description="number of periods"), + } + }, + ) + d = _dfn_to_plain_dict(dfn) + block = d["blocks"]["options"] + assert "nper" in block + assert block["nper"]["type"] == "integer" + assert block["nper"]["name"] == "nper" + + +def test_dfn_to_plain_dict_with_fieldv1_blocks(): + """FieldV1 blocks are serialized via dataclasses.asdict.""" + dfn = Dfn( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "save_flows": FieldV1(name="save_flows", type="keyword", block="options"), + } + }, + ) + d = _dfn_to_plain_dict(dfn) + block = d["blocks"]["options"] + assert "save_flows" in block + assert block["save_flows"]["name"] == "save_flows" + assert block["save_flows"]["type"] == "keyword" + + +def test_toml_safe_primitives_pass_through(): + """_toml_safe passes primitive types through unchanged.""" + assert _toml_safe("hello") == "hello" + assert _toml_safe(42) == 42 + assert _toml_safe(3.14) == 3.14 + assert _toml_safe(True) is True + assert _toml_safe(None) is None + + +def test_toml_safe_non_primitive_coerced_to_str(): + """_toml_safe coerces non-TOML-native types (e.g. Version) to str.""" + assert _toml_safe(Version("1.1")) == "1.1" + + +def test_toml_safe_fieldbase_via_model_dump(): + """_toml_safe converts FieldBase instances via model_dump recursively.""" + kw = Keyword(name="save_flows", description="save flows") + result = _toml_safe(kw) + assert isinstance(result, dict) + assert result["name"] == "save_flows" + assert result["type"] == "keyword" + + +def test_toml_safe_nested(): + """_toml_safe recurses into dicts and lists.""" + obj = {"a": [Version("2"), "plain"], "b": {"c": 99}} + result = _toml_safe(obj) + assert result["a"][0] == "2" + assert result["a"][1] == "plain" + assert result["b"]["c"] == 99 diff --git a/modflow_devtools/dfn/__init__.py b/modflow_devtools/dfn/__init__.py new file mode 100644 index 00000000..5c5807f8 --- /dev/null +++ b/modflow_devtools/dfn/__init__.py @@ -0,0 +1,34 @@ +""" +MODFLOW 6 definition file tools (v1 / v1.1 schema). +""" + +from modflow_devtools.dfn.v1 import ( + Dfn, + Dfns, + Field, + FieldType, + Fields, + FormatVersion, + Reader, + Ref, + Sln, + get_dfns, +) +from modflow_devtools.dfn.v1_1 import Dfn as DfnSpec +from modflow_devtools.dfn.v1_1 import FieldV1, FieldV1_1 + +__all__ = [ + "Dfn", + "DfnSpec", + "Dfns", + "Field", + "FieldType", + "FieldV1", + "FieldV1_1", + "Fields", + "FormatVersion", + "Reader", + "Ref", + "Sln", + "get_dfns", +] diff --git a/modflow_devtools/dfn/mapper.py b/modflow_devtools/dfn/mapper.py new file mode 100644 index 00000000..db9d036c --- /dev/null +++ b/modflow_devtools/dfn/mapper.py @@ -0,0 +1,346 @@ +""" +v1 / v1.1 schema mapping, I/O helpers, and serialization utilities. +""" + +from __future__ import annotations + +import dataclasses +from dataclasses import asdict +from itertools import groupby +from os import PathLike +from pathlib import Path +from typing import Any + +import tomli +from boltons.dictutils import OMD +from packaging.version import Version + +from modflow_devtools.dfn.parse import ( + is_advanced_package, + is_multi_package, + parse_dfn, + parse_mf6_subpackages, + try_parse_bool, + try_parse_parent, +) +from modflow_devtools.dfn.v1_1 import Dfn, Dfns, FieldV1, FieldV1_1 + + +# ============================================================================= +# Mappers +# ============================================================================= + + +class MapV1To1_1: + """Map a v1 Dfn (FieldV1 blocks) to a v1.1 Dfn (FieldV1_1 blocks).""" + + @staticmethod + def map_field(field: FieldV1) -> FieldV1_1: + return FieldV1_1( + name=field.name, + type=field.type, + block=field.block, + default=field.default, + longname=field.longname, + description=field.description, + optional=field.optional, + developmode=field.developmode, + shape=field.shape, + valid=field.valid, + netcdf=field.netcdf, + tagged=field.tagged, + ) + + def map(self, dfn: Dfn) -> Dfn: + blocks: dict[str, dict] = {} + for block_name, block_fields in (dfn.blocks or {}).items(): + blocks[block_name] = { + fname: MapV1To1_1.map_field(f) + for fname, f in block_fields.items() + if isinstance(f, FieldV1) + } + return dataclasses.replace( + dfn, + schema_version=Version("1.1"), + blocks=blocks if blocks else None, + ) + + +def map( + dfn: Dfn, + schema_version: "str | Version" = "1.1", +) -> Dfn: + """Map a MODFLOW 6 v1 definition to v1 or v1.1 schema.""" + version = Version(str(schema_version)) + if version == Version("1"): + raise NotImplementedError("Mapping to schema version 1 is not implemented.") + if version == Version("1.1"): + if dfn.schema_version >= Version("1.1"): + return dfn + return MapV1To1_1().map(dfn) + raise ValueError(f"Unsupported schema version: {schema_version!r}. Expected '1' or '1.1'.") + + +# ============================================================================= +# I/O helpers +# ============================================================================= + + +def load(f: Any, format: str = "dfn", **kwargs: Any) -> Dfn: + """Load a MODFLOW 6 definition file into a Dfn.""" + if format == "dfn": + name = kwargs.pop("name") + fields_parsed, meta = parse_dfn(f, **kwargs) + blocks = { + block_name: {field_dict["name"]: FieldV1.from_dict(field_dict) for field_dict in block} + for block_name, block in groupby( + fields_parsed.values(multi=True), lambda fd: fd["block"] + ) + } + subcomponents = parse_mf6_subpackages(meta) + return Dfn( + name=name, + schema_version=Version("1"), + parent=try_parse_parent(meta), + advanced=is_advanced_package(meta), + multi=is_multi_package(meta), + blocks=blocks, + subcomponents=subcomponents if subcomponents else None, + ) + + if format == "toml": + from modflow_devtools.dfns.schema.v2 import FieldBase + + data = tomli.load(f) + dfn_name = data.pop("name", kwargs.pop("name", None)) + + dfn_fields: dict[str, Any] = { + "name": dfn_name, + "schema_version": Version(str(data.pop("schema_version", "2"))), + "parent": data.pop("parent", None), + "advanced": data.pop("advanced", False), + "multi": data.pop("multi", False), + "ftype": data.pop("ftype", None), + } + # variant_of is a v2 Component concept; consume but don't store on Dfn + data.pop("variant_of", None) + + if (expected_name := kwargs.pop("name", None)) is not None: + if dfn_fields["name"] != expected_name: + raise ValueError( + f"DFN name mismatch: {expected_name} != {dfn_fields['name']}" + ) + + parsed_blocks: dict[str, Any] = {} + for section_name, section_data in data.items(): + if isinstance(section_data, dict): + block_fields: dict[str, Any] = {} + for field_name, field_data in section_data.items(): + if isinstance(field_data, dict): + block_fields[field_name] = FieldBase.from_dict(field_data) + else: + block_fields[field_name] = field_data + parsed_blocks[section_name] = block_fields + + dfn_fields["blocks"] = parsed_blocks if parsed_blocks else None + return Dfn(**dfn_fields) + + raise ValueError(f"Unsupported format: {format!r}. Expected 'dfn' or 'toml'.") + + +def _load_common(f: Any) -> Any: + common, _ = parse_dfn(f) + return common + + +def load_flat(path: "str | PathLike") -> Dfns: + """ + Load a flat MODFLOW 6 specification from definition files in a directory. + + Returns a dictionary of unlinked Dfns (children not populated). + """ + exclude = ["common", "flopy"] + path = Path(path).expanduser().resolve() + + dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.stem not in exclude} + toml_paths = {p.stem: p for p in path.glob("*.toml") if p.stem not in exclude} + dfns: Dfns = {} + if dfn_paths: + with (path / "common.dfn").open() as f: + common = _load_common(f) + for dfn_name, dfn_path in dfn_paths.items(): + with dfn_path.open() as f: + dfns[dfn_name] = load(f, name=dfn_name, common=common, format="dfn") + if toml_paths: + for toml_name, toml_path in toml_paths.items(): + with toml_path.open("rb") as f: + dfns[toml_name] = load(f, name=toml_name, format="toml") + return dfns + + +def _infer_parent(name: str) -> str | None: + """Infer the parent component name from a component name using MF6 conventions.""" + if name == "sim-nam": + return None + if name.endswith("-nam"): + return "sim-nam" + if name.startswith(("exg-", "sln-", "utl-")): + return "sim-nam" + if "-" in name: + mdl = name.split("-")[0] + return f"{mdl}-nam" + return None + + +def _resolve_parent_for_tree( + name: str, parent: "str | list[str] | None", dfns: Dfns +) -> "str | None": + """ + Resolve a parent value to a specific component name for tree placement. + + When parent is a type label or any string not present in the known component + dict, falls back to name-based inference. + """ + if parent is None: + return None + if isinstance(parent, str) and parent in dfns: + return parent + return _infer_parent(name) + + +def _apply_parent_inference(dfns: Dfns) -> Dfns: + """Set parent on any Dfn where it is not already explicit.""" + result: Dfns = {} + for name, dfn in dfns.items(): + if dfn.parent is None: + inferred = _infer_parent(name) + result[name] = dataclasses.replace(dfn, parent=inferred) if inferred else dfn + else: + result[name] = dfn + return result + + +def to_tree(dfns: Dfns) -> Dfn: + """ + Infer the MODFLOW 6 input component hierarchy from a flat spec. + + Returns the root component. There must be exactly one root (no parent). + """ + dfns = _apply_parent_inference(dfns) + first_dfn = next(iter(dfns.values()), None) + + match schema_version := str(first_dfn.schema_version if first_dfn else Version("1")): + case "1": + raise NotImplementedError("Tree inference from v1 schema not implemented") + case "1.1" | "2": + roots = {name: dfn for name, dfn in dfns.items() if dfn.parent is None} + if (nroots := len(roots)) != 1: + raise ValueError(f"Expected one root component, found {nroots}") + + def _build_tree(node_name: str) -> Dfn: + node = dfns[node_name] + children = { + name: dfn + for name, dfn in dfns.items() + if _resolve_parent_for_tree(name, dfn.parent, dfns) == node_name + } + if children: + node = dataclasses.replace( + node, + children={name: _build_tree(name) for name in children}, + ) + return node + + return _build_tree(next(iter(roots.keys()))) + case _: + raise ValueError( + f"Unsupported schema version: {schema_version!r}. Expected '1.1' or '2'." + ) + + +def to_flat(dfn: Dfn) -> Dfns: + """Flatten a MODFLOW 6 input component hierarchy to a flat spec.""" + + def _flatten(d: Dfn) -> Dfns: + result: Dfns = {d.name: dataclasses.replace(d, children=None)} + for child in (d.children or {}).values(): + result.update(_flatten(child)) + return result + + return _flatten(dfn) + + +def is_valid(path: "str | PathLike", format: str = "dfn", verbose: bool = False) -> bool: + """Validate DFN file(s).""" + path = Path(path).expanduser().absolute() + try: + if not path.exists(): + raise FileNotFoundError(f"Path does not exist: {path}") + + if path.is_file(): + common: Any = {} + if (common_path := path.parent / "common.dfn").exists(): + with common_path.open() as f: + common, _ = parse_dfn(f) + if path.name == "common.dfn": + return True + with path.open() as f: + load(f, name=path.stem, common=common, format=format) + else: + load_flat(path) + return True + except Exception as e: + if verbose: + print(f"Validation failed: {e}") + return False + + +# ============================================================================= +# Serialization helpers +# ============================================================================= + + +def _dfn_to_plain_dict(dfn: Dfn) -> dict: + """Serialize a Dfn (dataclass) to a plain Python dict.""" + from modflow_devtools.dfns.schema.v2 import FieldBase + + d: dict[str, Any] = {} + for field_name in dfn.__dataclass_fields__: + v = getattr(dfn, field_name) + if v is None: + continue + if isinstance(v, Version): + d[field_name] = str(v) + else: + d[field_name] = v + + if blocks := d.get("blocks"): + serialized: dict[str, dict] = {} + for block_name, block_fields in blocks.items(): + block_out: dict = {} + for field_name, field_val in block_fields.items(): + if isinstance(field_val, FieldBase): + block_out[field_name] = field_val.model_dump(exclude_none=True) + elif dataclasses.is_dataclass(field_val) and not isinstance(field_val, type): + block_out[field_name] = asdict(field_val) + else: + block_out[field_name] = field_val + serialized[block_name] = block_out + d["blocks"] = serialized + + return d + + +def _toml_safe(obj: Any) -> Any: + """Recursively coerce non-TOML-native types to str.""" + from modflow_devtools.dfns.schema.v2 import FieldBase + + if isinstance(obj, FieldBase): + return _toml_safe(obj.model_dump(exclude_none=True)) + if isinstance(obj, dict): + return {k: _toml_safe(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_toml_safe(v) for v in obj] + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + return str(obj) diff --git a/modflow_devtools/dfns/parse.py b/modflow_devtools/dfn/parse.py similarity index 100% rename from modflow_devtools/dfns/parse.py rename to modflow_devtools/dfn/parse.py diff --git a/modflow_devtools/dfn.py b/modflow_devtools/dfn/v1.py similarity index 100% rename from modflow_devtools/dfn.py rename to modflow_devtools/dfn/v1.py diff --git a/modflow_devtools/dfn/v1_1.py b/modflow_devtools/dfn/v1_1.py new file mode 100644 index 00000000..b90bfbda --- /dev/null +++ b/modflow_devtools/dfn/v1_1.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import dataclasses +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Literal + +from boltons.dictutils import OMD +from packaging.version import Version + + +@dataclass(kw_only=True) +class FieldV1_1: + name: str + type: str | None = None + block: str | None = None + default: Any | None = None + longname: str | None = None + description: str | None = None + children: Mapping[str, FieldV1_1] | None = None + optional: bool = False + developmode: bool = False + shape: str | None = None + valid: tuple[str, ...] | None = None + netcdf: bool = False + tagged: bool = False + + @classmethod + def from_dict(cls, d: dict, strict: bool = False) -> FieldV1_1: + keys = set(cls.__dataclass_fields__.keys()) + if strict: + if extra_keys := set(d.keys()) - keys: + raise ValueError(f"Unrecognized keys in field data: {extra_keys}") + return cls(**{k: v for k, v in d.items() if k in keys}) + + +Block = Mapping[str, FieldV1_1] +Blocks = Mapping[str, Block] + + +def block_sort_key(item: tuple[str, Any]) -> int: + """Sort blocks in canonical MF6 order.""" + order = ["options", "dimensions", "griddata", "packagedata", "connectiondata", "period"] + name = item[0] + try: + return order.index(name) + except ValueError: + return len(order) + + +FieldType = Literal[ + "keyword", + "integer", + "double precision", + "string", + "record", + "recarray", + "keystring", +] + +SCALAR_TYPES = ("keyword", "integer", "double precision", "string") + +Reader = Literal[ + "urword", + "u1ddbl", + "u2ddbl", + "readarray", +] + + +@dataclass(kw_only=True) +class FieldV1(FieldV1_1): + # V1-specific attributes + reader: Reader = "urword" + in_record: bool = False + layered: bool | None = None + preserve_case: bool = False + numeric_index: bool = False + deprecated: bool = False + removed: bool = False + mf6internal: str | None = None + block_variable: bool = False + just_data: bool = False + time_series: bool = False + + @classmethod + def from_dict(cls, d: dict, strict: bool = False) -> "FieldV1": + keys = set(cls.__dataclass_fields__.keys()) + if strict: + if extra_keys := set(d.keys()) - keys: + raise ValueError(f"Unrecognized keys in field data: {extra_keys}") + return cls(**{k: v for k, v in d.items() if k in keys}) + + +@dataclass(kw_only=True) +class Dfn: + """ + MODFLOW 6 input component definition (v1 / v1.1 schema). + + Attributes + ---------- + schema_version : Version + Schema version of this definition. + name : str + Component name (e.g., "gwf-chd", "sim-nam"). + parent : str | list[str] | None + Valid parent component type(s). + advanced : bool + Whether this is an advanced package. + multi : bool + Whether this is a multi-package. + ftype : str | None + MODFLOW 6 file type string, if applicable. + blocks : Blocks | None + Block definitions containing field specifications. + children : Dfns | None + Child component instances (populated by to_tree). + subcomponents : list[str] | None + Allowed child component types (schema-level constraint). + """ + + schema_version: Version + name: str + parent: str | list[str] | None = None + advanced: bool = False + multi: bool = False + ftype: str | None = None + blocks: Blocks | None = None + children: Dfns | None = None + subcomponents: list[str] | None = None + + def __post_init__(self) -> None: + self.schema_version = Version(str(self.schema_version)) + if self.blocks is not None: + self.blocks = dict(sorted(self.blocks.items(), key=block_sort_key)) + + @property + def fields(self) -> OMD: + """Combined map of fields from all blocks (flat, top-level only).""" + items = [] + for block in (self.blocks or {}).values(): + for f in block.values(): + items.append((f.name, f)) + return OMD(items) + + @classmethod + def from_dict(cls, d: dict, strict: bool = False) -> Dfn: + """ + Create a Dfn from a dictionary. + + Parameters + ---------- + d : dict + Dictionary containing DFN data. + strict : bool, optional + If True, raise ValueError for unrecognized keys at any level. + """ + from modflow_devtools.dfns.schema.v2 import FieldBase + + known_keys = set(cls.__dataclass_fields__.keys()) + schema_version = Version(str(d.get("schema_version", "2"))) + is_v1 = schema_version == Version("1") + + if strict: + extra = set(d.keys()) - known_keys + if extra: + raise ValueError(f"Unrecognized keys in DFN data: {extra}") + + data = {k: v for k, v in d.items() if k in known_keys} + data.setdefault("schema_version", schema_version) + + if blocks_raw := data.get("blocks"): + parsed_blocks: dict[str, Any] = {} + for block_name, block_data in blocks_raw.items(): + if not isinstance(block_data, dict): + parsed_blocks[block_name] = block_data + continue + block_fields: dict[str, Any] = {} + for field_name, field_data in block_data.items(): + if isinstance(field_data, dict): + if is_v1: + block_fields[field_name] = FieldV1.from_dict(field_data, strict=strict) + else: + block_fields[field_name] = FieldBase.from_dict( + field_data, strict=strict + ) + else: + block_fields[field_name] = field_data + parsed_blocks[block_name] = block_fields + data["blocks"] = parsed_blocks + + return cls(**data) + + +Dfns = dict[str, Dfn] diff --git a/modflow_devtools/dfn2toml.py b/modflow_devtools/dfn2toml.py index 96a68661..d0a7a3f6 100644 --- a/modflow_devtools/dfn2toml.py +++ b/modflow_devtools/dfn2toml.py @@ -1,46 +1,149 @@ -"""Convert DFNs to TOML.""" +"""Convert MODFLOW 6 DFN files to TOML (v1, v1.1, or v2 schema).""" import argparse +import sys +import textwrap from os import PathLike from pathlib import Path import tomli_w as tomli from boltons.iterutils import remap -from modflow_devtools.dfn import Dfn +from modflow_devtools.dfn.mapper import ( + _dfn_to_plain_dict, + _load_common, + _toml_safe, + is_valid, + load, + load_flat, + to_flat, + to_tree, +) +from modflow_devtools.dfn.v1_1 import Dfn +from modflow_devtools.misc import drop_none_or_empty # mypy: ignore-errors -def convert(indir: PathLike, outdir: PathLike): - indir = Path(indir).expanduser().absolute() +def convert(inpath: PathLike, outdir: PathLike, schema: str = "1") -> None: + """Convert DFN file(s) to TOML. + + Parameters + ---------- + inpath : PathLike + Input file or directory. + outdir : PathLike + Output directory. + schema : str + Target schema version: "1", "1.1", or "2". + """ + inpath = Path(inpath).expanduser().absolute() outdir = Path(outdir).expanduser().absolute() outdir.mkdir(exist_ok=True, parents=True) - for dfn in Dfn.load_all(indir).values(): - with Path.open(outdir / f"{dfn['name']}.toml", "wb") as f: - def drop_none_or_empty(path, key, value): - if value is None or value == "" or value == [] or value == {}: - return False - return True + if schema not in ("1", "1.1", "2"): + raise ValueError(f"Unsupported schema version: {schema!r}. Expected '1', '1.1', or '2'.") + + if inpath.is_file(): + if inpath.name == "common.dfn": + raise ValueError("Cannot convert common.dfn as a standalone file") + + common = {} + if (common_path := inpath.parent / "common.dfn").exists(): + with common_path.open() as f: + common = _load_common(f) + + with inpath.open() as f: + dfn = load(f, name=inpath.stem, common=common, format="dfn") + + _convert(dfn, outdir / f"{inpath.stem}.toml", schema=schema) + else: + if schema == "1": + # v1: iterate files directly (no tree building) + dfns = load_flat(inpath) + for dfn_name, dfn in dfns.items(): + _convert(dfn, outdir / f"{dfn_name}.toml", schema=schema) + else: + # v1.1 / v2: map all, build tree, flatten, convert + if schema == "1.1": + from modflow_devtools.dfn.mapper import map as map_v1_1 + + dfns = {name: map_v1_1(dfn, "1.1") for name, dfn in load_flat(inpath).items()} + else: + from modflow_devtools.dfns.mapper import map as map_v2 + + dfns = {name: map_v2(dfn, "2") for name, dfn in load_flat(inpath).items()} + + if schema == "1.1": + tree = to_tree(dfns) + flat = to_flat(tree) + for dfn_name, dfn in flat.items(): + _convert(dfn, outdir / f"{dfn_name}.toml", schema=schema) + else: + for dfn_name, component in dfns.items(): + _convert(component, outdir / f"{dfn_name}.toml", schema=schema) - tomli.dump(remap(dfn, visit=drop_none_or_empty), f) + +def _convert(dfn_or_component: object, outpath: Path, schema: str = "1") -> None: + with Path.open(outpath, "wb") as f: + if schema == "2": + # Component is a Pydantic model + d = dfn_or_component.model_dump(exclude_none=True) # type: ignore[union-attr] + tomli.dump(_toml_safe(remap(d, visit=drop_none_or_empty)), f) + else: + # Dfn dataclass (v1 or v1.1) + dfn_dict = _dfn_to_plain_dict(dfn_or_component) # type: ignore[arg-type] + if blocks := dfn_dict.pop("blocks", None): + for block_name, block_fields in blocks.items(): + dfn_dict.setdefault(block_name, {}) + for field_name, field_data in block_fields.items(): + dfn_dict[block_name][field_name] = field_data + tomli.dump(_toml_safe(remap(dfn_dict, visit=drop_none_or_empty)), f) if __name__ == "__main__": - """Convert DFN files to TOML.""" + parser = argparse.ArgumentParser( + description="Convert MODFLOW 6 DFN files to TOML.", + epilog=textwrap.dedent( + """\ +Convert MODFLOW 6 definition files (.dfn format) to TOML files. - parser = argparse.ArgumentParser(description="Convert DFN files to TOML.") +Schema versions: + 1 — v1 TOML: all original DFN attributes preserved, no schema change. + 1.1 — v1.1 TOML: v1-specific attributes stripped; shared base fields only. + 2 — v2 TOML: fully typed v2 Component schema (Pydantic model serialization). +""" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) parser.add_argument( "--indir", "-i", type=str, - help="Directory containing DFN files.", + help="Input file or directory containing DFN files.", ) parser.add_argument( "--outdir", "-o", help="Output directory.", ) + parser.add_argument( + "--schema", + "-s", + default="1", + choices=["1", "1.1", "2"], + help="Target schema version (default: 1).", + ) + parser.add_argument( + "--validate", + "-v", + action="store_true", + help="Validate DFN files without converting them.", + ) args = parser.parse_args() - convert(args.indir, args.outdir) + + if args.validate: + if not is_valid(args.indir): + sys.exit(1) + else: + convert(args.indir, args.outdir, schema=args.schema) diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index 89d1aa22..081185cf 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -2,36 +2,11 @@ MODFLOW 6 definition file tools. """ -import re import warnings -from abc import ABC, abstractmethod -from dataclasses import asdict -from itertools import groupby from os import PathLike from pathlib import Path -from typing import ( - Annotated, - Any, - Literal, - cast, -) - -import tomli -from boltons.dictutils import OMD -from packaging.version import Version -from pydantic import BaseModel, ConfigDict, GetCoreSchemaHandler -from pydantic_core import core_schema -from modflow_devtools.dfns.parse import ( - is_advanced_package, - is_multi_package, - parse_dfn, - parse_mf6_subpackages, - try_parse_bool, - try_parse_parent, -) -from modflow_devtools.dfns.schema.v1 import SCALAR_TYPES as V1_SCALAR_TYPES -from modflow_devtools.dfns.schema.v1 import FieldV1 +from modflow_devtools.dfn.v1_1 import FieldV1 from modflow_devtools.dfns.schema.v2 import ( Array, Block, @@ -51,9 +26,6 @@ from modflow_devtools.dfns.schema.v2 import ( Path as PathField, ) -from modflow_devtools.misc import try_literal_eval - -_IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") # Experimental API warning warnings.warn( @@ -71,13 +43,11 @@ "Block", "Blocks", "Component", - "Dfn", "DfnRegistry", "DfnRegistryDiscoveryError", "DfnRegistryError", "DfnRegistryNotFoundError", "DfnSpec", - "Dfns", "Double", "FieldBase", "FieldV1", @@ -97,1104 +67,29 @@ "get_sync_status", "is_valid", "list_components", - "load", - "load_flat", - "load_tree", - "map", "sync_dfns", - "to_flat", - "to_tree", ] -Format = Literal["dfn", "toml"] -"""DFN serialization format.""" - - -Dfns = dict[str, "Dfn"] - - -class _VersionAnnotation: - @classmethod - def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> Any: - return core_schema.no_info_plain_validator_function( - lambda v: Version(str(v)) if not isinstance(v, Version) else v, - serialization=core_schema.to_string_ser_schema(), - ) - - -VersionField = Annotated[Version, _VersionAnnotation] - - -class Dfn(BaseModel): - """ - MODFLOW 6 input component definition. - - Attributes - ---------- - schema_version : Version - Schema version of this definition. - name : str - Component name (e.g., "gwf-chd", "sim-nam"). - parent : str | list[str] | None - Valid parent component type(s). - advanced : bool - Whether this is an advanced package. - multi : bool - Whether this is a multi-package. - variant_of : str | None - If set, names the canonical component this is a format variant of. - blocks : dict[str, Any] | None - Block definitions containing field specifications. - children : dict[str, Dfn] | None - Child component instances (populated by to_tree). - subcomponents : list[str] | None - Allowed child component types (schema-level constraint). - """ - - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - - schema_version: VersionField - name: str - parent: str | list[str] | None = None - advanced: bool = False - multi: bool = False - variant_of: str | None = None - blocks: dict[str, Any] | None = None - children: "dict[str, Dfn] | None" = None - subcomponents: list[str] | None = None - - @property - def fields(self) -> Any: - """ - Combined map of fields from all blocks (flat, top-level only). - - Returns an OMD to support duplicate field names across v1 blocks. - """ - items = [] - for block in (self.blocks or {}).values(): - for f in block.values(): - items.append((f.name, f)) - return OMD(items) - - @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> "Dfn": - """ - Create a Dfn from a dictionary. - - Parameters - ---------- - d : dict - Dictionary containing DFN data. - strict : bool, optional - If True, raise ValueError for unrecognized keys at any level. - """ - known_keys = set(cls.model_fields.keys()) - schema_version = Version(str(d.get("schema_version", "2"))) - is_v1 = schema_version == Version("1") - - if strict: - extra = set(d.keys()) - known_keys - if extra: - raise ValueError(f"Unrecognized keys in DFN data: {extra}") - - data = {k: v for k, v in d.items() if k in known_keys} - data.setdefault("schema_version", schema_version) - - if blocks_raw := data.get("blocks"): - parsed_blocks: dict[str, Any] = {} - for block_name, block_data in blocks_raw.items(): - if not isinstance(block_data, dict): - parsed_blocks[block_name] = block_data - continue - block_fields: dict[str, Any] = {} - for field_name, field_data in block_data.items(): - if isinstance(field_data, dict): - if is_v1: - block_fields[field_name] = FieldV1.from_dict(field_data, strict=strict) - else: - block_fields[field_name] = FieldBase.from_dict( - field_data, strict=strict - ) - else: - block_fields[field_name] = field_data - parsed_blocks[block_name] = block_fields - data["blocks"] = parsed_blocks - - return cls.model_validate(data) - - -def _dfn_to_plain_dict(dfn: "Dfn") -> dict: - """Serialize a Dfn to a plain Python dict, serializing any Pydantic field instances.""" - d = dfn.model_dump(exclude_none=True) - if blocks := d.get("blocks"): - for block_name, block_fields in blocks.items(): - for field_name, field_val in list(block_fields.items()): - if isinstance(field_val, BaseModel): - blocks[block_name][field_name] = field_val.model_dump(exclude_none=True) - return d - - -def _toml_safe(obj: Any) -> Any: - """Recursively coerce non-TOML-native types to str.""" - if isinstance(obj, BaseModel): - return _toml_safe(obj.model_dump(exclude_none=True)) - if isinstance(obj, dict): - return {k: _toml_safe(v) for k, v in obj.items()} - if isinstance(obj, list): - return [_toml_safe(v) for v in obj] - if isinstance(obj, (str, int, float, bool)) or obj is None: - return obj - return str(obj) - - -class SchemaMap(ABC): - @abstractmethod - def map(self, dfn: Dfn) -> Dfn: ... - - -class MapV1To2(SchemaMap): - @staticmethod - def map_period_block(dfn: "Dfn", block: dict) -> dict: - """ - Convert a period block recarray to individual arrays, one per column. - """ - block = dict(block) - fields_list = list(block.values()) - - if fields_list and isinstance(fields_list[0], List): - assert len(fields_list) == 1 - list_field: List = fields_list[0] - block.pop(list_field.name) - item = list_field.item - columns: dict = dict(item.fields if isinstance(item, Record) else item.arms) - else: - columns = dict(block) - - cellid = columns.pop("cellid", None) - - _SCALAR_DTYPES = {"keyword", "integer", "double", "double precision", "string"} - - for col_name, column in columns.items(): - if isinstance(column, Array): - dtype = column.dtype - elif getattr(column, "type", None) in _SCALAR_DTYPES: - dtype = column.type - if dtype == "double precision": - dtype = "double" - else: - # Composite column (Record, Union, etc.) — keep as-is - block[col_name] = column - continue - - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - old_dims = list(column.shape) if isinstance(column, Array) else [] - new_dims = ["nper"] - if cellid: - new_dims.append("nodes") - # Only carry structural grid dims forward; runtime-only inline - # counts (e.g. naux) are not declared shape dims in v2. - new_dims.extend(d for d in old_dims if d in GRID_DIM_NAMESPACE) - - block[col_name] = Array( - name=column.name, - longname=getattr(column, "longname", None), - description=getattr(column, "description", None), - optional=column.optional, - default=getattr(column, "default", None), - developmode=column.developmode, - netcdf=getattr(column, "netcdf", False), - dtype=dtype, - shape=new_dims, - ) - - return block - - @staticmethod - def map_field(dfn: "Dfn", v1_field: FieldV1) -> FieldBase: - """ - Convert a v1 field to the appropriate v2 concrete type. - """ - fields = cast(OMD, dfn.fields) - - def _to_bool(v: Any, default: bool = False) -> bool: - if isinstance(v, bool): - return v - if isinstance(v, str): - s = v.strip().lower() - if s == "true": - return True - if s in ("false", ""): - return False - return default - - def _map_field(f: FieldV1) -> FieldBase: - fd = asdict(f) - fd = {k: try_parse_bool(v) for k, v in fd.items()} - - _name: str = fd["name"] - _type: str | None = fd.get("type") - shape_str: str | None = fd.get("shape") or None - description: str | None = fd.get("description") or None - longname: str | None = fd.get("longname") or None - optional: bool = _to_bool(fd.get("optional"), False) - developmode: bool = _to_bool(fd.get("developmode"), False) - netcdf: bool = _to_bool(fd.get("netcdf"), False) - tagged: bool = _to_bool(fd.get("tagged"), False) - preserve_case: bool = _to_bool(fd.get("preserve_case"), False) - time_series: bool = _to_bool(fd.get("time_series"), False) - valid = fd.get("valid") - _default_raw = fd.get("default") - default = ( - try_literal_eval(_default_raw) - if _type != "string" and isinstance(_default_raw, str) - else _default_raw - ) - - _COL_FK_RE = re.compile(r"^([A-Za-z_]\w*)\(([A-Za-z_]\w*)\)$") - - def _parse_shape(s: str) -> list[str]: - result = [] - s_clean = s.strip() - # Strip exactly one pair of outer parentheses to avoid munging - # nested forms like (ncon(ifno)). - if s_clean.startswith("(") and s_clean.endswith(")"): - s_clean = s_clean[1:-1] - for elem in (x.strip() for x in s_clean.split(",") if x.strip()): - if ";" in elem: - # v1 discretization-conditional (e.g. "ncol*nrow; ncpl") - # → canonical per-layer count; DIS derives ncpl = nrow*ncol. - result.append("ncpl") - elif ( - elem in ("any1d", "unknown") or elem.startswith("<") or elem.startswith(">") - ): - # v1 pseudo-elements with no v2 shape equivalent: - # any1d — inline array of runtime-determined length - # (read to end of record); dtype-agnostic. - # X — bound annotations (e.g. " once v1 DFN typos are fixed. - pass - elif m := _COL_FK_RE.fullmatch(elem): - # v1 shorthand: column(fk_field) with no block prefix. - # Resolve the block by searching for the integer field. - col_name = m.group(1) - block_name = next( - ( - fi.block - for fi in fields.values(multi=True) - if fi.name == col_name and fi.type == "integer" and fi.in_record - ), - None, - ) - if block_name: - result.append(f"{block_name}.{elem}") - # else: unresolvable; drop with no shape element - else: - # Check if elem is an implicit count (e.g. "naux") for a - # string array whose v1 shape is (elem). If so, emit the - # string array's name so _mark_string_dim_arrays can mark - # it dimension="component" and validation resolves it. - provider = next( - ( - fi.name - for fi in fields.values(multi=True) - if fi.type == "string" - and (fi.shape or "").strip() in (f"({elem})", elem) - ), - None, - ) - result.append(provider if provider else elem) - return result - - def _to_scalar() -> FieldBase: - assert _type is not None - if _type == "keyword": - return Keyword( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - ) - if _type == "string": - return String( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - tagged=tagged, - valid=list(valid) if valid else None, - case_sensitive=preserve_case, - time_series=time_series, - ) - if _type == "integer": - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - v = [int(x) for x in valid] if valid else None - if fd.get("block") == "dimensions": - if _name in GRID_DIM_NAMESPACE: - _dim_scope: ( - Literal["record", "component", "model", "simulation"] | None - ) = "model" - elif dfn.name == "sim-tdis" and _name == "nper": - _dim_scope = "simulation" - else: - _dim_scope = "component" - else: - _dim_scope = None - return Integer( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - tagged=tagged, - valid=v, - time_series=time_series, - dimension=_dim_scope, - ) - if _type in ("double", "double precision"): - return Double( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - tagged=tagged, - time_series=time_series, - ) - raise TypeError(f"Unsupported scalar type: {_type!r}") - - def _row_field() -> "Record | Union": - item_names = (_type or "").split()[1:] - if not item_names: - raise ValueError(f"Missing list item definition: {_type!r}") - - item_types = [ - fi.type - for fi in fields.values(multi=True) - if fi.name in item_names and fi.in_record - ] - - # Single explicit record or keystring - if ( - len(item_names) == 1 - and item_types - and ( - (item_types[0] or "").startswith("record") - or (item_types[0] or "").startswith("keystring") - ) - ): - mapped = MapV1To2.map_field(dfn, next(iter(fields.getlist(item_names[0])))) - if isinstance(mapped, (Record, Union)): - return mapped - raise TypeError( - f"Expected Record or Union for list item, got {type(mapped).__name__}" - ) - - # All scalars → implicit Record - if all(t in V1_SCALAR_TYPES for t in item_types): - rec_fields = _record_fields() - return Record( - name=_name, - description=( - (description or "").replace("is the list of", "is the record of") - or None - ), - fields=rec_fields, - ) - - # Mixed composites - children = { - fi.name: MapV1To2.map_field(dfn, fi) - for fi in fields.values(multi=True) - if fi.name in item_names and fi.in_record - } - first = next(iter(children.values())) - if len(children) == 1 and isinstance(first, Union): - return first - return Record( - name=_name, - description=( - (description or "").replace("is the list of", "is the record of") or None - ), - fields=children, # type: ignore[arg-type] - ) - - def _union_fields() -> dict: - names = (_type or "").split()[1:] - return { - fi.name: MapV1To2.map_field(dfn, fi) - for fi in fields.values(multi=True) - if fi.name in names and fi.in_record - } - - def _record_fields() -> dict: - names = (_type or "").split()[1:] - result = {} - for rname in names: - matches = [ - fi - for fi in fields.values(multi=True) - if fi.name == rname - and fi.in_record - and not (fi.type or "").startswith("record") - ] - if matches: - result[rname] = _map_field(matches[0]) - return result - - if _type is None: - raise ValueError(f"Missing type for v1 field: {_name!r}") - - if _type.startswith("recarray"): - item = _row_field() - return List( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - item=item, - ) - - if _type.startswith("keystring"): - arms = _union_fields() - return Union( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - arms=arms, # type: ignore[arg-type] - ) - - if _type.startswith("record"): - rec_fields = _record_fields() - return Record( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - fields=rec_fields, # type: ignore[arg-type] - ) - - if shape_str is not None: - dtype_map: dict[str, Literal["keyword", "integer", "double", "string"]] = { - "double precision": "double", - "double": "double", - "integer": "integer", - "string": "string", - "keyword": "keyword", - } - dtype = dtype_map.get(_type) - if dtype is not None: - if dtype == "string": - # String arrays are inline and self-sizing in v2; drop - # the v1 shape expression. dimension=True is set in a - # second pass if another array references this field. - return Array( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - time_series=time_series, - dtype="string", - shape=[], - ) - parsed_shape = _parse_shape(shape_str) - return Array( - name=_name, - longname=longname, - description=description, - optional=optional, - default=default, - developmode=developmode, - netcdf=netcdf, - time_series=time_series, - dtype=dtype, - shape=parsed_shape, - ) - - return _to_scalar() - - return _map_field(v1_field) - - @staticmethod - def _mark_dimension_fields(blocks: "dict[str, dict]") -> "dict[str, dict]": - """ - Post-pass: annotate every field that provides a dimension count. - - Two concerns are handled in one pass because they share a scan phase - and have an ordering dependency — string-array dim providers must be - known before record-local dim integers can be identified (so their - names are excluded from the "local" check). - - String-array dim providers (e.g. ``auxiliary``): a string Array whose - name is referenced as a plain identifier in any non-string Array's - shape. Marked ``dimension="component"``. - - Record-local dim integers: an Integer field inside a Record that is - named by a sibling non-string Array's shape element and does not - resolve to any globally-scoped dim. Marked ``dimension="record"``. - """ - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - # ── Phase 1: single scan ────────────────────────────────────────── - # Collect plain-identifier shape refs from non-string Arrays, names - # of all string Arrays, and any already-explicit global dim names. - shape_refs: set[str] = set() - string_array_names: set[str] = set() - explicit_globals: set[str] = set(GRID_DIM_NAMESPACE) - - def _scan(fields: dict) -> None: - for f in fields.values(): - if isinstance(f, Array): - if f.dtype != "string": - for elem in f.shape: - if _IDENT_RE.fullmatch(elem): - shape_refs.add(elem) - else: - string_array_names.add(f.name) - scope = getattr(f, "dimension", None) - if scope in ("component", "model", "simulation"): - explicit_globals.add(f.name) - if isinstance(f, Record): - _scan(f.fields) - elif isinstance(f, Union): - _scan(f.arms) - elif isinstance(f, List): - item = f.item - _scan(item.fields if isinstance(item, Record) else item.arms) - - for block_fields in blocks.values(): - _scan(block_fields) - - if not shape_refs: - return blocks - - # ── Phase 2: derive complete global dim set ─────────────────────── - # String arrays referenced by name in a non-string array's shape are - # dim providers; add them to global_dims so they are not mistakenly - # identified as record-local dims in the mark phase below. - string_provider_names: set[str] = string_array_names & shape_refs - global_dims: set[str] = explicit_globals | string_provider_names - - # ── Phase 3: mark ───────────────────────────────────────────────── - def _record_local_dims(rec: "Record") -> set[str]: - """Integer field names in rec that serve as per-row inline counts.""" - to_mark: set[str] = set() - for sf in rec.fields.values(): - if isinstance(sf, Array) and sf.dtype != "string": - for elem in sf.shape: - if _IDENT_RE.fullmatch(elem) and elem not in global_dims: - sibling = rec.fields.get(elem) - if isinstance(sibling, Integer) and sibling.dimension is None: - to_mark.add(elem) - return to_mark - - def _mark(fields: dict) -> dict: - result = {} - for name, f in fields.items(): - if isinstance(f, Array) and f.dtype == "string" and name in string_provider_names: - f = f.model_copy(update={"dimension": "component"}) - elif isinstance(f, Record): - local_dims = _record_local_dims(f) - new_fields = _mark(f.fields) - if local_dims: - new_fields = { - fn: ( - sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf - ) - for fn, sf in new_fields.items() - } - f = f.model_copy(update={"fields": new_fields}) - elif isinstance(f, Union): - f = f.model_copy(update={"arms": _mark(f.arms)}) - elif isinstance(f, List): - item = f.item - new_item: Record | Union - if isinstance(item, Record): - local_dims = _record_local_dims(item) - new_item_fields = _mark(item.fields) - if local_dims: - new_item_fields = { - fn: ( - sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf - ) - for fn, sf in new_item_fields.items() - } - new_item = item.model_copy(update={"fields": new_item_fields}) - else: - new_item = item.model_copy(update={"arms": _mark(item.arms)}) - f = f.model_copy(update={"item": new_item}) - result[name] = f - return result - - return {bn: _mark(bf) for bn, bf in blocks.items()} - - @staticmethod - def _infer_fk_from_shapes(blocks: "dict[str, dict]") -> "dict[str, dict]": - """ - Post-pass 3: infer fk= and pk= from resolved lookup shape elements. - - When _parse_shape resolves a v1 shorthand like "col(fk_field)" to the - canonical form "block.col(fk_field)", the fk_field sibling in the same - enclosing record implicitly references that block. This pass: - - infers fk = "block.fk_field" on that sibling (enables check 4 of - _validate_shape_element), and - - infers pk = True on the same-named field in the referenced block's - list item (required by _validate_fk_fields). - Both marks are only applied when the attribute is not already set. - """ - _lookup_re = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") - - # Phase 1: collect inferred FK and PK pairs. - # fk_map: (enclosing_block, fk_field_name) -> fk value string - # pk_set: (pk_block, pk_field_name) pairs that need pk=True - fk_map: dict[tuple[str, str], str] = {} - pk_set: set[tuple[str, str]] = set() - - def _scan_record(rec: "Record", block_name: str) -> None: - for sf in rec.fields.values(): - if isinstance(sf, Array): - for elem in sf.shape: - m = _lookup_re.fullmatch(elem) - if m: - pk_block, _col, fk_fname = m.groups() - sibling = rec.fields.get(fk_fname) - if sibling is not None and getattr(sibling, "fk", None) is None: - fk_map[(block_name, fk_fname)] = f"{pk_block}.{fk_fname}" - pk_set.add((pk_block, fk_fname)) - - def _scan(fields: dict, block_name: str) -> None: - for f in fields.values(): - if isinstance(f, Record): - _scan_record(f, block_name) - elif isinstance(f, Union): - _scan(f.arms, block_name) - elif isinstance(f, List): - item = f.item - if isinstance(item, Record): - _scan_record(item, block_name) - elif isinstance(item, Union): - _scan(item.arms, block_name) - - for block_name, block_fields in blocks.items(): - _scan(block_fields, block_name) - - if not fk_map and not pk_set: - return blocks - - # Phase 2: apply FK and PK markings. - def _apply_record(rec: "Record", block_name: str) -> "Record": - updates: dict = {} - for fname, sf in rec.fields.items(): - updated = sf - if (block_name, fname) in fk_map and getattr(sf, "fk", None) is None: - updated = updated.model_copy(update={"fk": fk_map[(block_name, fname)]}) - if (block_name, fname) in pk_set and not getattr(sf, "pk", False): - updated = updated.model_copy(update={"pk": True}) - if updated is not sf: - updates[fname] = updated - if not updates: - return rec - return rec.model_copy( - update={"fields": {fn: updates.get(fn, sf) for fn, sf in rec.fields.items()}} - ) - - def _apply(fields: dict, block_name: str) -> dict: - result = {} - for name, f in fields.items(): - if isinstance(f, Record): - f = _apply_record(f, block_name) - elif isinstance(f, Union): - f = f.model_copy(update={"arms": _apply(f.arms, block_name)}) - elif isinstance(f, List): - item = f.item - new_item: Record | Union - if isinstance(item, Record): - new_item = _apply_record(item, block_name) - else: - new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) - f = f.model_copy(update={"item": new_item}) - result[name] = f - return result - - return {bn: _apply(bf, bn) for bn, bf in blocks.items()} - - @staticmethod - def map_blocks(dfn: "Dfn") -> dict: - """ - Convert all v1 fields in a DFN to v2 types and return a block dict. - - Structured as three phases; phases 2 and 3 are post-passes because - cross-field relationships cannot be determined field-by-field: - - 1. Field conversion (``map_field`` per top-level field): translate - each FieldV1 to the appropriate v2 concrete type. Per-field only; - cross-field relationships are deferred. - - 2. Dimension annotation (``_mark_dimension_fields``): scan the - completed blocks and annotate every dimension-providing field: - string Arrays referenced by non-string Array shapes (→ - ``dimension="component"``) and Record-local inline-count Integers - (→ ``dimension="record"``). - - 3. FK/PK inference (``_infer_fk_from_shapes``): scan for resolved - ``block.col(fk_field)`` shape elements and infer the corresponding - ``fk=`` and ``pk=`` annotations. Only applies to components with - implicit cross-block FK relationships (gwf-sfr in v1). - """ - all_v1 = cast(OMD, dfn.fields) - grouped: dict[str, dict] = {} - for v1_field in all_v1.values(multi=True): - if v1_field.in_record: # type: ignore[attr-defined] - continue - block_name = v1_field.block - mapped = MapV1To2.map_field(dfn, v1_field) - grouped.setdefault(block_name, {})[v1_field.name] = mapped - - blocks: dict[str, dict] = {} - if period := grouped.pop("period", None): - blocks["period"] = MapV1To2.map_period_block(dfn, period) - for block_name, block_data in grouped.items(): - blocks[block_name] = block_data - - blocks = MapV1To2._mark_dimension_fields(blocks) - return MapV1To2._infer_fk_from_shapes(blocks) - - @staticmethod - def to_component(dfn: "Dfn") -> "Any": - """ - Convert a v2 Dfn to the appropriate Component - (Simulation, Model, or Package), inferring the type from the - component name and parsed metadata. - - The Dfn must already be at schema version 2 (field types must be - v2 concrete FieldBase instances, not FieldV1 dataclasses). - """ - from modflow_devtools.dfns.schema.v2 import ( - Block, - Model, - Package, - Simulation, - ) - - name = dfn.name - blocks: dict[str, Block] | None = None - if dfn.blocks: - blocks = { - block_name: Block( - name=block_name, - fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, # type: ignore[misc] - ) - for block_name, block_fields in dfn.blocks.items() - if isinstance(block_fields, dict) - } - - common: dict[str, Any] = { - "name": name, - "blocks": blocks, - "parent": dfn.parent, - "schema_version": dfn.schema_version, - } - if name == "sim-nam": - return Simulation(**common) - if name.endswith("-nam"): - return Model(**common) - if name.startswith("sln-"): - return Package(**common, subtype="solution", multi=dfn.multi, variant_of=dfn.variant_of) - if name.startswith("exg-"): - return Package(**common, subtype="exchange", multi=dfn.multi, variant_of=dfn.variant_of) - if name.startswith("utl-"): - return Package(**common, subtype="utility", multi=dfn.multi, variant_of=dfn.variant_of) - has_period = bool(blocks and any("period" in k for k in blocks)) - subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = ( - "advanced" if dfn.advanced else "stress" if has_period else None - ) - return Package(**common, subtype=subtype, multi=dfn.multi, variant_of=dfn.variant_of) - - def map(self, dfn: "Dfn") -> "Dfn": - if dfn.schema_version == Version("2"): - return dfn - return Dfn( - name=dfn.name, - schema_version=Version("2"), - parent=dfn.parent, - advanced=dfn.advanced, - multi=dfn.multi, - variant_of=dfn.variant_of, - blocks=MapV1To2.map_blocks(dfn), - subcomponents=dfn.subcomponents, - ) - - -def map( - dfn: Dfn, - schema_version: str | Version = "2", -) -> Dfn: - """Map a MODFLOW 6 specification to another schema version.""" - version = Version(str(schema_version)) - if version == dfn.schema_version: - return dfn - elif version == Version("1"): - raise NotImplementedError("Mapping to schema version 1 is not implemented yet.") - elif version == Version("2"): - return MapV1To2().map(dfn) - raise ValueError(f"Unsupported schema version: {schema_version}. Expected 1 or 2.") - - -def load(f, format: str = "dfn", **kwargs) -> Dfn: - """Load a MODFLOW 6 definition file.""" - if format == "dfn": - name = kwargs.pop("name") - fields_parsed, meta = parse_dfn(f, **kwargs) - blocks = { - block_name: {field_dict["name"]: FieldV1.from_dict(field_dict) for field_dict in block} - for block_name, block in groupby( - fields_parsed.values(multi=True), lambda fd: fd["block"] - ) - } - subcomponents = parse_mf6_subpackages(meta) - return Dfn( - name=name, - schema_version=Version("1"), - parent=try_parse_parent(meta), - advanced=is_advanced_package(meta), - multi=is_multi_package(meta), - blocks=blocks, - subcomponents=subcomponents if subcomponents else None, - ) - - elif format == "toml": - data = tomli.load(f) - dfn_name = data.pop("name", kwargs.pop("name", None)) - - dfn_fields: dict[str, Any] = { - "name": dfn_name, - "schema_version": Version(str(data.pop("schema_version", "2"))), - "parent": data.pop("parent", None), - "advanced": data.pop("advanced", False), - "multi": data.pop("multi", False), - "variant_of": data.pop("variant_of", None), - } - - if (expected_name := kwargs.pop("name", None)) is not None: - if dfn_fields["name"] != expected_name: - raise ValueError(f"DFN name mismatch: {expected_name} != {dfn_fields['name']}") - - blocks = {} - for section_name, section_data in data.items(): - if isinstance(section_data, dict): - block_fields: dict[str, Any] = {} - for field_name, field_data in section_data.items(): - if isinstance(field_data, dict): - block_fields[field_name] = FieldBase.from_dict(field_data) - else: - block_fields[field_name] = field_data - blocks[section_name] = block_fields - - dfn_fields["blocks"] = blocks if blocks else None - return Dfn(**dfn_fields) - - raise ValueError(f"Unsupported format: {format}. Expected 'dfn' or 'toml'.") - - -def _load_common(f) -> Any: - common, _ = parse_dfn(f) - return common - - -def load_flat(path: str | PathLike) -> Dfns: - """ - Load a flat MODFLOW 6 specification from definition files in a directory. - - Returns a dictionary of unlinked DFNs, i.e. without `children` populated. - """ - exclude = ["common", "flopy"] - path = Path(path).expanduser().resolve() - - dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.stem not in exclude} - toml_paths = {p.stem: p for p in path.glob("*.toml") if p.stem not in exclude} - dfns = {} - if dfn_paths: - with (path / "common.dfn").open() as f: - common = _load_common(f) - for dfn_name, dfn_path in dfn_paths.items(): - with dfn_path.open() as f: - dfns[dfn_name] = load(f, name=dfn_name, common=common, format="dfn") - if toml_paths: - for toml_name, toml_path in toml_paths.items(): - with toml_path.open("rb") as f: - dfns[toml_name] = load(f, name=toml_name, format="toml") - return dfns - - -def load_tree(path: str | PathLike) -> Dfn: - """ - Load a structured MODFLOW 6 specification from definition files in a directory. - - A single root component definition (the simulation) is returned with - nested children. - """ - return to_tree(load_flat(path)) - - -def _infer_parent(name: str) -> "str | None": - """Infer the parent component name from a component name using MF6 conventions.""" - if name == "sim-nam": - return None - if name.endswith("-nam"): - return "sim-nam" - if name.startswith(("exg-", "sln-", "utl-")): - return "sim-nam" - if "-" in name: - mdl = name.split("-")[0] - return f"{mdl}-nam" - return None - - -def _resolve_parent_for_tree(name: str, parent: "str | list[str] | None", dfns: Dfns) -> "str | None": - """ - Resolve a parent value to a specific component name for tree placement. - - When parent is a type label (e.g. "model", ["model", "package"]) or any - string not present in the known component dict, falls back to name-based - inference so the DFN is still placed in the tree. - """ - if parent is None: - return None - if isinstance(parent, str) and parent in dfns: - return parent - return _infer_parent(name) - - -def _apply_parent_inference(dfns: Dfns) -> Dfns: - """Set parent on any Dfn where it is not already explicit.""" - result = {} - for name, dfn in dfns.items(): - if dfn.parent is None: - inferred = _infer_parent(name) - result[name] = dfn.model_copy(update={"parent": inferred}) if inferred else dfn - else: - result[name] = dfn - return result - - -def to_tree(dfns: Dfns) -> Dfn: - """ - Infer the MODFLOW 6 input component hierarchy from a flat spec. - - Returns the root component. There must be exactly one root (no parent). - Assumes DFNs are already in v2 schema. - """ - dfns = _apply_parent_inference(dfns) - first_dfn = next(iter(dfns.values()), None) - - match schema_version := str(first_dfn.schema_version if first_dfn else Version("1")): - case "1": - raise NotImplementedError("Tree inference from v1 schema not implemented") - case "2": - roots = {name: dfn for name, dfn in dfns.items() if dfn.parent is None} - if (nroots := len(roots)) != 1: - raise ValueError(f"Expected one root component, found {nroots}") - - def _build_tree(node_name: str) -> Dfn: - node = dfns[node_name] - children = { - name: dfn - for name, dfn in dfns.items() - if _resolve_parent_for_tree(name, dfn.parent, dfns) == node_name - } - if children: - node = node.model_copy( - update={"children": {name: _build_tree(name) for name in children}} - ) - return node - - return _build_tree(next(iter(roots.keys()))) - case _: - raise ValueError(f"Unsupported schema version: {schema_version}. Expected 1 or 2.") - - -def to_flat(dfn: Dfn) -> Dfns: - """ - Flatten a MODFLOW 6 input component hierarchy to a flat spec. - - Returns a dictionary of all components without `children` populated. - """ - - def _flatten(dfn: Dfn) -> Dfns: - result: Dfns = {dfn.name: dfn.model_copy(update={"children": None})} - for child in (dfn.children or {}).values(): - result.update(_flatten(child)) - return result - - return _flatten(dfn) - - -def is_valid(path: str | PathLike, format: str = "dfn", verbose: bool = False) -> bool: +def is_valid(path: "str | PathLike", format: str = "dfn", verbose: bool = False) -> bool: """Validate DFN file(s).""" - path = Path(path).expanduser().absolute() - try: - if not path.exists(): - raise FileNotFoundError(f"Path does not exist: {path}") + from modflow_devtools.dfn.mapper import is_valid as _is_valid - if path.is_file(): - common = {} # type: ignore - if (common_path := path.parent / "common.dfn").exists(): - with common_path.open() as f: - common, _ = parse_dfn(f) - if path.name == "common.dfn": - return True - with path.open() as f: - load(f, name=path.stem, common=common, format=format) - else: - load_flat(path) - return True - except Exception as e: - if verbose: - print(f"Validation failed: {e}") - return False + return _is_valid(path, format=format, verbose=verbose) # ============================================================================= -# Registry imports and convenience functions +# Registry lazy-import machinery # ============================================================================= def _get_registry_module(): - """Lazy import of registry module to avoid circular imports.""" from modflow_devtools.dfns import registry return registry def __getattr__(name: str): - """Lazy attribute access for registry classes.""" registry_exports = { "DfnRegistry", "DfnRegistryDiscoveryError", @@ -1221,11 +116,9 @@ def get_dfn( component: str, ref: str = "develop", source: str = "modflow6", - path: str | PathLike | None = None, + path: "str | PathLike | None" = None, ) -> "Component": - """ - Get a component definition by name from the registry. - """ + """Get a component definition by name from the registry.""" registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) return reg.get_dfn(component) @@ -1235,11 +128,9 @@ def get_dfn_path( component: str, ref: str = "develop", source: str = "modflow6", - path: str | PathLike | None = None, + path: "str | PathLike | None" = None, ) -> Path: - """ - Get the local cached file path for a DFN component. - """ + """Get the local cached file path for a DFN component.""" registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) return reg.get_dfn_path(component) @@ -1248,11 +139,9 @@ def get_dfn_path( def list_components( ref: str = "develop", source: str = "modflow6", - path: str | PathLike | None = None, + path: "str | PathLike | None" = None, ) -> list[str]: - """ - List available components for a registry. - """ + """List available components for a registry.""" registry = _get_registry_module() reg = registry.get_registry(source=source, ref=ref, path=path) return list(reg.spec.components.keys()) diff --git a/modflow_devtools/dfns/dfn2toml.py b/modflow_devtools/dfns/dfn2toml.py deleted file mode 100644 index 10ebc71f..00000000 --- a/modflow_devtools/dfns/dfn2toml.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Convert DFNs to TOML.""" - -import argparse -import sys -import textwrap -from os import PathLike -from pathlib import Path - -import tomli_w as tomli -from boltons.iterutils import remap - -from modflow_devtools.dfns import ( - Dfn, - _dfn_to_plain_dict, - _toml_safe, - is_valid, - load, - load_flat, - map, - to_flat, - to_tree, -) -from modflow_devtools.dfns.parse import parse_dfn -from modflow_devtools.misc import drop_none_or_empty - -# mypy: ignore-errors - - -def convert(inpath: PathLike, outdir: PathLike, schema_version: str = "2") -> None: - """ - Convert DFN files in `inpath` to TOML files in `outdir`. - By default, convert the definitions to schema version 2. - """ - inpath = Path(inpath).expanduser().absolute() - outdir = Path(outdir).expanduser().absolute() - outdir.mkdir(exist_ok=True, parents=True) - - if inpath.is_file(): - if inpath.name == "common.dfn": - raise ValueError("Cannot convert common.dfn as a standalone file") - - common_path = inpath.parent / "common.dfn" - if common_path.exists(): - with common_path.open() as f: - common, _ = parse_dfn(f) - else: - common = {} - - with inpath.open() as f: - dfn = load(f, name=inpath.stem, common=common, format="dfn") - - dfn = map(dfn, schema_version=schema_version) - _convert(dfn, outdir / f"{inpath.stem}.toml") - else: - dfns = { - name: map(dfn, schema_version=schema_version) for name, dfn in load_flat(inpath).items() - } - tree = to_tree(dfns) - flat = to_flat(tree) - for dfn_name, dfn in flat.items(): - _convert(dfn, outdir / f"{dfn_name}.toml") - - -def _convert(dfn: Dfn, outpath: Path) -> None: - with Path.open(outpath, "wb") as f: - dfn_dict = _dfn_to_plain_dict(dfn) - if blocks := dfn_dict.pop("blocks", None): - for block_name, block_fields in blocks.items(): - dfn_dict.setdefault(block_name, {}) - for field_name, field_data in block_fields.items(): - dfn_dict[block_name][field_name] = field_data - - tomli.dump(_toml_safe(remap(dfn_dict, visit=drop_none_or_empty)), f) - - -if __name__ == "__main__": - """ - Convert DFN files in the original format and schema version 1 - to TOML files, by default also converting to schema version 2. - """ - - parser = argparse.ArgumentParser( - description="Convert DFN files to TOML.", - epilog=textwrap.dedent( - """\ -Convert DFN files in the original format and schema version 1 -to TOML files, by default also converting to schema version 2. -""" - ), - ) - parser.add_argument( - "--indir", - "-i", - type=str, - help="Directory containing DFN files, or a single DFN file.", - ) - parser.add_argument( - "--outdir", - "-o", - help="Output directory.", - ) - parser.add_argument( - "--schema-version", - "-s", - type=str, - default="2", - help="Schema version to convert to.", - ) - parser.add_argument( - "--validate", - "-v", - action="store_true", - help="Validate DFN files without converting them.", - ) - args = parser.parse_args() - - if args.validate: - if not is_valid(args.indir): - sys.exit(1) - else: - convert(args.indir, args.outdir, args.schema_version) diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py new file mode 100644 index 00000000..3add04de --- /dev/null +++ b/modflow_devtools/dfns/mapper.py @@ -0,0 +1,693 @@ +""" +v2 schema mapping for MODFLOW 6 DFNs. +""" + +from __future__ import annotations + +import dataclasses +import re +from dataclasses import asdict +from typing import Any, Literal, cast + +from boltons.dictutils import OMD +from packaging.version import Version + +from modflow_devtools.dfn.parse import ( + is_advanced_package, + is_multi_package, + parse_dfn, + parse_mf6_subpackages, + try_parse_bool, + try_parse_parent, +) +from modflow_devtools.dfn.v1_1 import SCALAR_TYPES as V1_SCALAR_TYPES +from modflow_devtools.dfn.v1_1 import Dfn, Dfns, FieldV1 +from modflow_devtools.dfns.schema.v2 import ( + Array, + Double, + FieldBase, + Integer, + Keyword, + List, + Record, + String, + Union, +) +from modflow_devtools.dfns.schema.v2 import Path as PathField +from modflow_devtools.misc import try_literal_eval + +_IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") + + +# ============================================================================= +# Mapper +# ============================================================================= + + +class MapV1To2: + """Map a v1 Dfn (FieldV1 blocks) to a v2 Component.""" + + @staticmethod + def map_period_block(dfn: Dfn, block: dict) -> dict: + """Convert a period block recarray to individual arrays, one per column.""" + block = dict(block) + fields_list = list(block.values()) + + if fields_list and isinstance(fields_list[0], List): + assert len(fields_list) == 1 + list_field: List = fields_list[0] + block.pop(list_field.name) + item = list_field.item + columns: dict = dict(item.fields if isinstance(item, Record) else item.arms) + else: + columns = dict(block) + + cellid = columns.pop("cellid", None) + + _SCALAR_DTYPES = {"keyword", "integer", "double", "double precision", "string"} + + for col_name, column in columns.items(): + if isinstance(column, Array): + dtype = column.dtype + elif getattr(column, "type", None) in _SCALAR_DTYPES: + dtype = column.type + if dtype == "double precision": + dtype = "double" + else: + block[col_name] = column + continue + + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + + old_dims = list(column.shape) if isinstance(column, Array) else [] + new_dims = ["nper"] + if cellid: + new_dims.append("nodes") + new_dims.extend(d for d in old_dims if d in GRID_DIM_NAMESPACE) + + block[col_name] = Array( + name=column.name, + longname=getattr(column, "longname", None), + description=getattr(column, "description", None), + optional=column.optional, + default=getattr(column, "default", None), + developmode=column.developmode, + netcdf=getattr(column, "netcdf", False), + dtype=dtype, + shape=new_dims, + ) + + return block + + @staticmethod + def map_field(dfn: Dfn, v1_field: FieldV1) -> FieldBase: + """Convert a v1 field to the appropriate v2 concrete type.""" + fields = cast(OMD, dfn.fields) + + def _to_bool(v: Any, default: bool = False) -> bool: + if isinstance(v, bool): + return v + if isinstance(v, str): + s = v.strip().lower() + if s == "true": + return True + if s in ("false", ""): + return False + return default + + def _map_field(f: FieldV1) -> FieldBase: + fd = asdict(f) + fd = {k: try_parse_bool(v) for k, v in fd.items()} + + _name: str = fd["name"] + _type: str | None = fd.get("type") + shape_str: str | None = fd.get("shape") or None + description: str | None = fd.get("description") or None + longname: str | None = fd.get("longname") or None + optional: bool = _to_bool(fd.get("optional"), False) + developmode: bool = _to_bool(fd.get("developmode"), False) + netcdf: bool = _to_bool(fd.get("netcdf"), False) + tagged: bool = _to_bool(fd.get("tagged"), False) + preserve_case: bool = _to_bool(fd.get("preserve_case"), False) + time_series: bool = _to_bool(fd.get("time_series"), False) + valid = fd.get("valid") + _default_raw = fd.get("default") + default = ( + try_literal_eval(_default_raw) + if _type != "string" and isinstance(_default_raw, str) + else _default_raw + ) + + _COL_FK_RE = re.compile(r"^([A-Za-z_]\w*)\(([A-Za-z_]\w*)\)$") + + def _parse_shape(s: str) -> list[str]: + result = [] + s_clean = s.strip() + if s_clean.startswith("(") and s_clean.endswith(")"): + s_clean = s_clean[1:-1] + for elem in (x.strip() for x in s_clean.split(",") if x.strip()): + if ";" in elem: + result.append("ncpl") + elif ( + elem in ("any1d", "unknown") + or elem.startswith("<") + or elem.startswith(">") + ): + pass + elif m := _COL_FK_RE.fullmatch(elem): + col_name = m.group(1) + block_name = next( + ( + fi.block + for fi in fields.values(multi=True) + if fi.name == col_name + and fi.type == "integer" + and fi.in_record + ), + None, + ) + if block_name: + result.append(f"{block_name}.{elem}") + else: + provider = next( + ( + fi.name + for fi in fields.values(multi=True) + if fi.type == "string" + and (fi.shape or "").strip() in (f"({elem})", elem) + ), + None, + ) + result.append(provider if provider else elem) + return result + + def _to_scalar() -> FieldBase: + assert _type is not None + if _type == "keyword": + return Keyword( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + ) + if _type == "string": + return String( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + tagged=tagged, + valid=list(valid) if valid else None, + case_sensitive=preserve_case, + time_series=time_series, + ) + if _type == "integer": + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + + v = [int(x) for x in valid] if valid else None + if fd.get("block") == "dimensions": + if _name in GRID_DIM_NAMESPACE: + _dim_scope: ( + Literal["record", "component", "model", "simulation"] | None + ) = "model" + elif dfn.name == "sim-tdis" and _name == "nper": + _dim_scope = "simulation" + else: + _dim_scope = "component" + else: + _dim_scope = None + return Integer( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + tagged=tagged, + valid=v, + time_series=time_series, + dimension=_dim_scope, + ) + if _type in ("double", "double precision"): + return Double( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + tagged=tagged, + time_series=time_series, + ) + raise TypeError(f"Unsupported scalar type: {_type!r}") + + def _row_field() -> Record | Union: + item_names = (_type or "").split()[1:] + if not item_names: + raise ValueError(f"Missing list item definition: {_type!r}") + + item_types = [ + fi.type + for fi in fields.values(multi=True) + if fi.name in item_names and fi.in_record + ] + + if ( + len(item_names) == 1 + and item_types + and ( + (item_types[0] or "").startswith("record") + or (item_types[0] or "").startswith("keystring") + ) + ): + mapped = MapV1To2.map_field(dfn, next(iter(fields.getlist(item_names[0])))) + if isinstance(mapped, (Record, Union)): + return mapped + raise TypeError( + f"Expected Record or Union for list item, got {type(mapped).__name__}" + ) + + if all(t in V1_SCALAR_TYPES for t in item_types): + rec_fields = _record_fields() + return Record( + name=_name, + description=( + (description or "").replace("is the list of", "is the record of") + or None + ), + fields=rec_fields, + ) + + children = { + fi.name: MapV1To2.map_field(dfn, fi) + for fi in fields.values(multi=True) + if fi.name in item_names and fi.in_record + } + first = next(iter(children.values())) + if len(children) == 1 and isinstance(first, Union): + return first + return Record( + name=_name, + description=( + (description or "").replace("is the list of", "is the record of") or None + ), + fields=children, # type: ignore[arg-type] + ) + + def _union_fields() -> dict: + names = (_type or "").split()[1:] + return { + fi.name: MapV1To2.map_field(dfn, fi) + for fi in fields.values(multi=True) + if fi.name in names and fi.in_record + } + + def _record_fields() -> dict: + names = (_type or "").split()[1:] + result = {} + for rname in names: + matches = [ + fi + for fi in fields.values(multi=True) + if fi.name == rname + and fi.in_record + and not (fi.type or "").startswith("record") + ] + if matches: + result[rname] = _map_field(matches[0]) + return result + + if _type is None: + raise ValueError(f"Missing type for v1 field: {_name!r}") + + if _type.startswith("recarray"): + item = _row_field() + return List( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + item=item, + ) + + if _type.startswith("keystring"): + arms = _union_fields() + return Union( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + arms=arms, # type: ignore[arg-type] + ) + + if _type.startswith("record"): + rec_fields = _record_fields() + return Record( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + fields=rec_fields, # type: ignore[arg-type] + ) + + if shape_str is not None: + dtype_map: dict[str, Literal["keyword", "integer", "double", "string"]] = { + "double precision": "double", + "double": "double", + "integer": "integer", + "string": "string", + "keyword": "keyword", + } + dtype = dtype_map.get(_type) + if dtype is not None: + if dtype == "string": + return Array( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + time_series=time_series, + dtype="string", + shape=[], + ) + parsed_shape = _parse_shape(shape_str) + return Array( + name=_name, + longname=longname, + description=description, + optional=optional, + default=default, + developmode=developmode, + netcdf=netcdf, + time_series=time_series, + dtype=dtype, + shape=parsed_shape, + ) + + return _to_scalar() + + return _map_field(v1_field) + + @staticmethod + def _mark_dimension_fields(blocks: dict[str, dict]) -> dict[str, dict]: + """ + Post-pass: annotate every field that provides a dimension count. + + String-array dim providers (e.g. ``auxiliary``): marked + ``dimension="component"``. Record-local dim integers: marked + ``dimension="record"``. + """ + from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE + + shape_refs: set[str] = set() + string_array_names: set[str] = set() + explicit_globals: set[str] = set(GRID_DIM_NAMESPACE) + + def _scan(fields: dict) -> None: + for f in fields.values(): + if isinstance(f, Array): + if f.dtype != "string": + for elem in f.shape: + if _IDENT_RE.fullmatch(elem): + shape_refs.add(elem) + else: + string_array_names.add(f.name) + scope = getattr(f, "dimension", None) + if scope in ("component", "model", "simulation"): + explicit_globals.add(f.name) + if isinstance(f, Record): + _scan(f.fields) + elif isinstance(f, Union): + _scan(f.arms) + elif isinstance(f, List): + item = f.item + _scan(item.fields if isinstance(item, Record) else item.arms) + + for block_fields in blocks.values(): + _scan(block_fields) + + if not shape_refs: + return blocks + + string_provider_names: set[str] = string_array_names & shape_refs + global_dims: set[str] = explicit_globals | string_provider_names + + def _record_local_dims(rec: Record) -> set[str]: + to_mark: set[str] = set() + for sf in rec.fields.values(): + if isinstance(sf, Array) and sf.dtype != "string": + for elem in sf.shape: + if _IDENT_RE.fullmatch(elem) and elem not in global_dims: + sibling = rec.fields.get(elem) + if isinstance(sibling, Integer) and sibling.dimension is None: + to_mark.add(elem) + return to_mark + + def _mark(fields: dict) -> dict: + result = {} + for name, f in fields.items(): + if isinstance(f, Array) and f.dtype == "string" and name in string_provider_names: + f = f.model_copy(update={"dimension": "component"}) + elif isinstance(f, Record): + local_dims = _record_local_dims(f) + new_fields = _mark(f.fields) + if local_dims: + new_fields = { + fn: ( + sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf + ) + for fn, sf in new_fields.items() + } + f = f.model_copy(update={"fields": new_fields}) + elif isinstance(f, Union): + f = f.model_copy(update={"arms": _mark(f.arms)}) + elif isinstance(f, List): + item = f.item + new_item: Record | Union + if isinstance(item, Record): + local_dims = _record_local_dims(item) + new_item_fields = _mark(item.fields) + if local_dims: + new_item_fields = { + fn: ( + sf.model_copy(update={"dimension": "record"}) + if fn in local_dims and isinstance(sf, Integer) + else sf + ) + for fn, sf in new_item_fields.items() + } + new_item = item.model_copy(update={"fields": new_item_fields}) + else: + new_item = item.model_copy(update={"arms": _mark(item.arms)}) + f = f.model_copy(update={"item": new_item}) + result[name] = f + return result + + return {bn: _mark(bf) for bn, bf in blocks.items()} + + @staticmethod + def _infer_fk_from_shapes(blocks: dict[str, dict]) -> dict[str, dict]: + """Post-pass: infer fk= and pk= from resolved lookup shape elements.""" + _lookup_re = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") + + fk_map: dict[tuple[str, str], str] = {} + pk_set: set[tuple[str, str]] = set() + + def _scan_record(rec: Record, block_name: str) -> None: + for sf in rec.fields.values(): + if isinstance(sf, Array): + for elem in sf.shape: + m = _lookup_re.fullmatch(elem) + if m: + pk_block, _col, fk_fname = m.groups() + sibling = rec.fields.get(fk_fname) + if sibling is not None and getattr(sibling, "fk", None) is None: + fk_map[(block_name, fk_fname)] = f"{pk_block}.{fk_fname}" + pk_set.add((pk_block, fk_fname)) + + def _scan(fields: dict, block_name: str) -> None: + for f in fields.values(): + if isinstance(f, Record): + _scan_record(f, block_name) + elif isinstance(f, Union): + _scan(f.arms, block_name) + elif isinstance(f, List): + item = f.item + if isinstance(item, Record): + _scan_record(item, block_name) + elif isinstance(item, Union): + _scan(item.arms, block_name) + + for block_name, block_fields in blocks.items(): + _scan(block_fields, block_name) + + if not fk_map and not pk_set: + return blocks + + def _apply_record(rec: Record, block_name: str) -> Record: + updates: dict = {} + for fname, sf in rec.fields.items(): + updated = sf + if (block_name, fname) in fk_map and getattr(sf, "fk", None) is None: + updated = updated.model_copy(update={"fk": fk_map[(block_name, fname)]}) + if (block_name, fname) in pk_set and not getattr(sf, "pk", False): + updated = updated.model_copy(update={"pk": True}) + if updated is not sf: + updates[fname] = updated + if not updates: + return rec + return rec.model_copy( + update={"fields": {fn: updates.get(fn, sf) for fn, sf in rec.fields.items()}} + ) + + def _apply(fields: dict, block_name: str) -> dict: + result = {} + for name, f in fields.items(): + if isinstance(f, Record): + f = _apply_record(f, block_name) + elif isinstance(f, Union): + f = f.model_copy(update={"arms": _apply(f.arms, block_name)}) + elif isinstance(f, List): + item = f.item + new_item: Record | Union + if isinstance(item, Record): + new_item = _apply_record(item, block_name) + else: + new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) + f = f.model_copy(update={"item": new_item}) + result[name] = f + return result + + return {bn: _apply(bf, bn) for bn, bf in blocks.items()} + + @staticmethod + def map_blocks(dfn: Dfn) -> dict[str, dict]: + """ + Convert all v1 fields in a Dfn to v2 types and return a block dict. + + Three phases: + 1. Field conversion (map_field per top-level field). + 2. Dimension annotation (_mark_dimension_fields). + 3. FK/PK inference (_infer_fk_from_shapes). + """ + all_v1 = cast(OMD, dfn.fields) + grouped: dict[str, dict] = {} + for v1_field in all_v1.values(multi=True): + if v1_field.in_record: # type: ignore[attr-defined] + continue + block_name = v1_field.block + mapped = MapV1To2.map_field(dfn, v1_field) + grouped.setdefault(block_name, {})[v1_field.name] = mapped + + blocks: dict[str, dict] = {} + if period := grouped.pop("period", None): + blocks["period"] = MapV1To2.map_period_block(dfn, period) + for block_name, block_data in grouped.items(): + blocks[block_name] = block_data + + blocks = MapV1To2._mark_dimension_fields(blocks) + return MapV1To2._infer_fk_from_shapes(blocks) + + @staticmethod + def to_component(dfn: Dfn) -> Any: + """ + Convert a Dfn to the appropriate Component (Simulation, Model, or Package). + + For v1-mapped Dfns, variant_of is inferred from the component name: names + ending in "g" (grid variant) or "a" (array variant) are treated as variants + of the same name without the suffix (e.g. "gwf-welg" → "gwf-wel"). + """ + from modflow_devtools.dfns.schema.v2 import ( + Block, + Model, + Package, + Simulation, + ) + + name = dfn.name + blocks: dict[str, Block] | None = None + if dfn.blocks: + blocks = { + block_name: Block( + name=block_name, + fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, # type: ignore[misc] + ) + for block_name, block_fields in dfn.blocks.items() + if isinstance(block_fields, dict) + } + + def _infer_variant_of(n: str) -> str | None: + if n.endswith(("g", "a")): + return n[:-1] + return None + + common: dict[str, Any] = { + "name": name, + "blocks": blocks, + "parent": dfn.parent, + "schema_version": dfn.schema_version, + } + if name == "sim-nam": + return Simulation(**common) + if name.endswith("-nam"): + return Model(**common) + if name.startswith("sln-"): + return Package(**common, subtype="solution", multi=dfn.multi) + if name.startswith("exg-"): + return Package(**common, subtype="exchange", multi=dfn.multi) + if name.startswith("utl-"): + return Package( + **common, + subtype="utility", + multi=dfn.multi, + variant_of=_infer_variant_of(name), + ) + has_period = bool(blocks and any("period" in k for k in blocks)) + subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = ( + "advanced" if dfn.advanced else "stress" if has_period else None + ) + return Package( + **common, + subtype=subtype, + multi=dfn.multi, + variant_of=_infer_variant_of(name), + ) + + def map(self, dfn: Dfn) -> Any: + """Map a v1 (or v2) Dfn to a v2 Component.""" + if dfn.schema_version == Version("2"): + return MapV1To2.to_component(dfn) + mapped_blocks = MapV1To2.map_blocks(dfn) + temp = dataclasses.replace(dfn, schema_version=Version("2"), blocks=mapped_blocks) + return MapV1To2.to_component(temp) + + +def map( + dfn: Dfn, + schema_version: "str | Version" = "2", +) -> Any: + """Map a MODFLOW 6 definition to v2 schema.""" + version = Version(str(schema_version)) + if version == Version("2"): + return MapV1To2().map(dfn) + raise ValueError(f"Unsupported schema version: {schema_version!r}. Expected '2'.") diff --git a/modflow_devtools/dfns/schema/v1.py b/modflow_devtools/dfns/schema/v1.py deleted file mode 100644 index 292fc52d..00000000 --- a/modflow_devtools/dfns/schema/v1.py +++ /dev/null @@ -1,59 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Literal - -FieldType = Literal[ - "keyword", - "integer", - "double precision", - "string", - "record", - "recarray", - "keystring", -] - -SCALAR_TYPES = ("keyword", "integer", "double precision", "string") - - -Reader = Literal[ - "urword", - "u1ddbl", - "u2ddbl", - "readarray", -] - - -@dataclass(kw_only=True) -class FieldV1: - # Shared base attributes (inlined from the deleted field.py) - name: str - type: str | None = None - block: str | None = None - default: Any | None = None - longname: str | None = None - description: str | None = None - optional: bool = False - developmode: bool = False - shape: str | None = None - valid: tuple[str, ...] | None = None - netcdf: bool = False - tagged: bool = False - # V1-specific attributes - reader: Reader = "urword" - in_record: bool = False - layered: bool | None = None - preserve_case: bool = False - numeric_index: bool = False - deprecated: bool = False - removed: bool = False - mf6internal: str | None = None - block_variable: bool = False - just_data: bool = False - time_series: bool = False - - @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> "FieldV1": - keys = set(cls.__dataclass_fields__.keys()) - if strict: - if extra_keys := set(d.keys()) - keys: - raise ValueError(f"Unrecognized keys in field data: {extra_keys}") - return cls(**{k: v for k, v in d.items() if k in keys}) diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema/v2.py index 619dfe2a..49ea2fe5 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema/v2.py @@ -752,21 +752,12 @@ def load( path: "str | PathLike", schema_version: "str | Version | None" = None, ) -> "DfnSpec": - """ - Load a DfnSpec from a directory of DFN or TOML files. - - Component types are inferred from component names using the MF6 - naming conventions. This is a transitional implementation; when the - v1→v2 mapper is complete it will be replaced by a proper mapper call. - """ + """Load a DfnSpec from a directory of DFN or TOML files.""" from pathlib import Path as _Path - from modflow_devtools.dfns import ( - MapV1To2, - _apply_parent_inference, - load_flat, - ) - from modflow_devtools.dfns import map as map_dfn + from modflow_devtools.dfn.mapper import _apply_parent_inference, load_flat + from modflow_devtools.dfns.mapper import MapV1To2 + from modflow_devtools.dfns.mapper import map as map_dfn _path = _Path(path).expanduser().resolve() dfns = load_flat(_path) @@ -776,9 +767,8 @@ def load( first = next(iter(dfns.values())) if first.schema_version == Version("1"): dfns = _apply_parent_inference(dfns) - dfns = {n: map_dfn(d, "2") for n, d in dfns.items()} components: dict[str, Component] = { - name: MapV1To2.to_component(dfn) for name, dfn in dfns.items() + n: map_dfn(d, "2") for n, d in dfns.items() } return cls(components=components) From b4ccbb5b28b08d27dfdc327401fdab4f9a855c15 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 20:01:57 -0700 Subject: [PATCH 07/29] ruff --- autotest/test_dfn.py | 2 +- autotest/test_dfns.py | 9 ++++----- modflow_devtools/dfn/__init__.py | 2 +- modflow_devtools/dfn/mapper.py | 17 ++++++----------- modflow_devtools/dfn/v1_1.py | 3 +-- modflow_devtools/dfn2toml.py | 1 - modflow_devtools/dfns/mapper.py | 18 ++++-------------- modflow_devtools/dfns/schema/v2.py | 5 +---- 8 files changed, 18 insertions(+), 39 deletions(-) diff --git a/autotest/test_dfn.py b/autotest/test_dfn.py index f32ef9d6..1028d0b5 100644 --- a/autotest/test_dfn.py +++ b/autotest/test_dfn.py @@ -1,7 +1,6 @@ from pathlib import Path import pytest -from packaging.version import Version from modflow_devtools.dfn import Dfn, get_dfns from modflow_devtools.dfn2toml import convert @@ -105,6 +104,7 @@ def test_convert_v2(toml_v2_name): except ImportError: import tomli as tomllib # type: ignore[no-redef] from pydantic import TypeAdapter + from modflow_devtools.dfns.schema.v2 import Component with (TOML_V2_DIR / f"{toml_v2_name}.toml").open("rb") as f: diff --git a/autotest/test_dfns.py b/autotest/test_dfns.py index e7874841..ac9ee9d2 100644 --- a/autotest/test_dfns.py +++ b/autotest/test_dfns.py @@ -11,14 +11,15 @@ _toml_safe, load, load_flat, - map as map_v1_1, to_flat, to_tree, ) +from modflow_devtools.dfn.mapper import ( + map as map_v1_1, +) from modflow_devtools.dfn.v1_1 import Dfn, FieldV1, FieldV1_1 from modflow_devtools.dfns import is_valid from modflow_devtools.dfns.fetch import fetch_dfns -from modflow_devtools.dfns.mapper import MapV1To2 from modflow_devtools.dfns.mapper import map as map_v2 from modflow_devtools.dfns.schema.v2 import ( Array, @@ -673,9 +674,7 @@ def test_apply_parent_inference(): def test_apply_parent_inference_does_not_overwrite_explicit(): """_apply_parent_inference does not overwrite an already-set parent.""" dfns = { - "gwf-dis": Dfn( - schema_version=Version("1.1"), name="gwf-dis", parent="custom-parent" - ), + "gwf-dis": Dfn(schema_version=Version("1.1"), name="gwf-dis", parent="custom-parent"), } inferred = _apply_parent_inference(dfns) assert inferred["gwf-dis"].parent == "custom-parent" diff --git a/modflow_devtools/dfn/__init__.py b/modflow_devtools/dfn/__init__.py index 5c5807f8..c4c29d01 100644 --- a/modflow_devtools/dfn/__init__.py +++ b/modflow_devtools/dfn/__init__.py @@ -6,8 +6,8 @@ Dfn, Dfns, Field, - FieldType, Fields, + FieldType, FormatVersion, Reader, Ref, diff --git a/modflow_devtools/dfn/mapper.py b/modflow_devtools/dfn/mapper.py index db9d036c..33f23def 100644 --- a/modflow_devtools/dfn/mapper.py +++ b/modflow_devtools/dfn/mapper.py @@ -12,7 +12,6 @@ from typing import Any import tomli -from boltons.dictutils import OMD from packaging.version import Version from modflow_devtools.dfn.parse import ( @@ -20,12 +19,10 @@ is_multi_package, parse_dfn, parse_mf6_subpackages, - try_parse_bool, try_parse_parent, ) from modflow_devtools.dfn.v1_1 import Dfn, Dfns, FieldV1, FieldV1_1 - # ============================================================================= # Mappers # ============================================================================= @@ -68,7 +65,7 @@ def map(self, dfn: Dfn) -> Dfn: def map( dfn: Dfn, - schema_version: "str | Version" = "1.1", + schema_version: str | Version = "1.1", ) -> Dfn: """Map a MODFLOW 6 v1 definition to v1 or v1.1 schema.""" version = Version(str(schema_version)) @@ -127,9 +124,7 @@ def load(f: Any, format: str = "dfn", **kwargs: Any) -> Dfn: if (expected_name := kwargs.pop("name", None)) is not None: if dfn_fields["name"] != expected_name: - raise ValueError( - f"DFN name mismatch: {expected_name} != {dfn_fields['name']}" - ) + raise ValueError(f"DFN name mismatch: {expected_name} != {dfn_fields['name']}") parsed_blocks: dict[str, Any] = {} for section_name, section_data in data.items(): @@ -153,7 +148,7 @@ def _load_common(f: Any) -> Any: return common -def load_flat(path: "str | PathLike") -> Dfns: +def load_flat(path: str | PathLike) -> Dfns: """ Load a flat MODFLOW 6 specification from definition files in a directory. @@ -193,8 +188,8 @@ def _infer_parent(name: str) -> str | None: def _resolve_parent_for_tree( - name: str, parent: "str | list[str] | None", dfns: Dfns -) -> "str | None": + name: str, parent: str | list[str] | None, dfns: Dfns +) -> str | None: """ Resolve a parent value to a specific component name for tree placement. @@ -270,7 +265,7 @@ def _flatten(d: Dfn) -> Dfns: return _flatten(dfn) -def is_valid(path: "str | PathLike", format: str = "dfn", verbose: bool = False) -> bool: +def is_valid(path: str | PathLike, format: str = "dfn", verbose: bool = False) -> bool: """Validate DFN file(s).""" path = Path(path).expanduser().absolute() try: diff --git a/modflow_devtools/dfn/v1_1.py b/modflow_devtools/dfn/v1_1.py index b90bfbda..b88424f6 100644 --- a/modflow_devtools/dfn/v1_1.py +++ b/modflow_devtools/dfn/v1_1.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Literal @@ -84,7 +83,7 @@ class FieldV1(FieldV1_1): time_series: bool = False @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> "FieldV1": + def from_dict(cls, d: dict, strict: bool = False) -> FieldV1: keys = set(cls.__dataclass_fields__.keys()) if strict: if extra_keys := set(d.keys()) - keys: diff --git a/modflow_devtools/dfn2toml.py b/modflow_devtools/dfn2toml.py index d0a7a3f6..7e5b5cdc 100644 --- a/modflow_devtools/dfn2toml.py +++ b/modflow_devtools/dfn2toml.py @@ -19,7 +19,6 @@ to_flat, to_tree, ) -from modflow_devtools.dfn.v1_1 import Dfn from modflow_devtools.misc import drop_none_or_empty # mypy: ignore-errors diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index 3add04de..5192afa1 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -13,15 +13,10 @@ from packaging.version import Version from modflow_devtools.dfn.parse import ( - is_advanced_package, - is_multi_package, - parse_dfn, - parse_mf6_subpackages, try_parse_bool, - try_parse_parent, ) from modflow_devtools.dfn.v1_1 import SCALAR_TYPES as V1_SCALAR_TYPES -from modflow_devtools.dfn.v1_1 import Dfn, Dfns, FieldV1 +from modflow_devtools.dfn.v1_1 import Dfn, FieldV1 from modflow_devtools.dfns.schema.v2 import ( Array, Double, @@ -33,7 +28,6 @@ String, Union, ) -from modflow_devtools.dfns.schema.v2 import Path as PathField from modflow_devtools.misc import try_literal_eval _IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") @@ -149,9 +143,7 @@ def _parse_shape(s: str) -> list[str]: if ";" in elem: result.append("ncpl") elif ( - elem in ("any1d", "unknown") - or elem.startswith("<") - or elem.startswith(">") + elem in ("any1d", "unknown") or elem.startswith("<") or elem.startswith(">") ): pass elif m := _COL_FK_RE.fullmatch(elem): @@ -160,9 +152,7 @@ def _parse_shape(s: str) -> list[str]: ( fi.block for fi in fields.values(multi=True) - if fi.name == col_name - and fi.type == "integer" - and fi.in_record + if fi.name == col_name and fi.type == "integer" and fi.in_record ), None, ) @@ -684,7 +674,7 @@ def map(self, dfn: Dfn) -> Any: def map( dfn: Dfn, - schema_version: "str | Version" = "2", + schema_version: str | Version = "2", ) -> Any: """Map a MODFLOW 6 definition to v2 schema.""" version = Version(str(schema_version)) diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema/v2.py index 49ea2fe5..049a77c4 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema/v2.py @@ -756,7 +756,6 @@ def load( from pathlib import Path as _Path from modflow_devtools.dfn.mapper import _apply_parent_inference, load_flat - from modflow_devtools.dfns.mapper import MapV1To2 from modflow_devtools.dfns.mapper import map as map_dfn _path = _Path(path).expanduser().resolve() @@ -768,7 +767,5 @@ def load( if first.schema_version == Version("1"): dfns = _apply_parent_inference(dfns) - components: dict[str, Component] = { - n: map_dfn(d, "2") for n, d in dfns.items() - } + components: dict[str, Component] = {n: map_dfn(d, "2") for n, d in dfns.items()} return cls(components=components) From b0a72f4af29e2cb265848b6bacd7318ced0e742b Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 20:09:40 -0700 Subject: [PATCH 08/29] ruff ruff --- modflow_devtools/dfn/mapper.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modflow_devtools/dfn/mapper.py b/modflow_devtools/dfn/mapper.py index 33f23def..b32ab68a 100644 --- a/modflow_devtools/dfn/mapper.py +++ b/modflow_devtools/dfn/mapper.py @@ -187,9 +187,7 @@ def _infer_parent(name: str) -> str | None: return None -def _resolve_parent_for_tree( - name: str, parent: str | list[str] | None, dfns: Dfns -) -> str | None: +def _resolve_parent_for_tree(name: str, parent: str | list[str] | None, dfns: Dfns) -> str | None: """ Resolve a parent value to a specific component name for tree placement. From ebc5a1c114fcefc92f0b88488475cdc4cbfff0a7 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 20:15:56 -0700 Subject: [PATCH 09/29] less is more --- docs/md/dfn-schema.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/md/dfn-schema.md b/docs/md/dfn-schema.md index 4372dc2e..aec32044 100644 --- a/docs/md/dfn-schema.md +++ b/docs/md/dfn-schema.md @@ -85,11 +85,9 @@ ## Overview -A MODFLOW 6 simulation consists of a hierarchy of modules, each module representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. +A MODFLOW 6 simulation consists of a hierarchy of components, each one representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. -This document distinguishes **modules**, conceptual units of functionality as defined in the MF6 IO guide, from **components**: particular representations of modules. - -Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component and its fields, relationships between fields or to other components, and data representations and in some cases formatting information. Component definitions should not be expected to map 1-1 to modules. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. +Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component and its fields, relationships between fields or to other components, and data representations and in some cases formatting information. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. ## Components From 96f62b1136ec17f16a2e96403dd6c9be4da71916 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 13 May 2026 20:17:49 -0700 Subject: [PATCH 10/29] mypy --- modflow_devtools/programs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modflow_devtools/programs/__init__.py b/modflow_devtools/programs/__init__.py index 1de99248..afd49cf1 100644 --- a/modflow_devtools/programs/__init__.py +++ b/modflow_devtools/programs/__init__.py @@ -836,7 +836,7 @@ def download_archive( if github_token: headers["Authorization"] = f"token {github_token}" - response = requests.get(url, headers=headers, stream=True, timeout=30) + response = requests.get(url, headers=headers, stream=True, timeout=30) # type: ignore response.raise_for_status() # Write to temporary file first From a8c7c9bf1fecd7f4a137d7ba7b718de949345857 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Tue, 19 May 2026 18:27:32 -0700 Subject: [PATCH 11/29] big cleanup --- .github/workflows/ci.yml | 2 +- autotest/dfn/test_dfn.py | 488 +++++++++ autotest/dfn/test_mapper.py | 97 ++ autotest/dfns/conftest.py | 34 + autotest/dfns/test_dfns.py | 145 +++ autotest/dfns/test_dfns_registry.py | 66 ++ autotest/{ => dfns}/test_dfns_schema.py | 634 +++++++++--- autotest/dfns/test_mapper.py | 10 + autotest/test_dfn.py | 112 --- autotest/test_dfns.py | 926 ------------------ autotest/test_dfns_registry.py | 851 ---------------- autotest/test_models.py | 76 +- docs/md/dfns.md | 70 +- docs/md/{dfn-schema.md => dfnspec.md} | 70 +- modflow_devtools/cli.py | 4 +- modflow_devtools/dfn/__init__.py | 35 +- modflow_devtools/dfn/mapper.py | 367 +------ modflow_devtools/dfn/{parse.py => parser.py} | 4 +- modflow_devtools/dfn/{v1.py => schema.py} | 287 ++++-- modflow_devtools/dfn/v1_1.py | 194 ---- modflow_devtools/dfn2toml.py | 172 ++-- modflow_devtools/dfns/__init__.py | 111 +-- modflow_devtools/dfns/__main__.py | 235 +---- modflow_devtools/dfns/dfns.toml | 24 +- modflow_devtools/dfns/fetch.py | 25 - modflow_devtools/dfns/make_registry.py | 184 ---- modflow_devtools/dfns/mapper.py | 679 +++++-------- modflow_devtools/dfns/registry.py | 844 +++------------- .../dfns/{schema/v2.py => schema.py} | 263 ++--- modflow_devtools/models/__init__.py | 121 +-- modflow_devtools/models/__main__.py | 10 +- 31 files changed, 2427 insertions(+), 4713 deletions(-) create mode 100644 autotest/dfn/test_dfn.py create mode 100644 autotest/dfn/test_mapper.py create mode 100644 autotest/dfns/conftest.py create mode 100644 autotest/dfns/test_dfns.py create mode 100644 autotest/dfns/test_dfns_registry.py rename autotest/{ => dfns}/test_dfns_schema.py (57%) create mode 100644 autotest/dfns/test_mapper.py delete mode 100644 autotest/test_dfn.py delete mode 100644 autotest/test_dfns.py delete mode 100644 autotest/test_dfns_registry.py rename docs/md/{dfn-schema.md => dfnspec.md} (86%) rename modflow_devtools/dfn/{parse.py => parser.py} (98%) rename modflow_devtools/dfn/{v1.py => schema.py} (74%) delete mode 100644 modflow_devtools/dfn/v1_1.py delete mode 100644 modflow_devtools/dfns/fetch.py delete mode 100644 modflow_devtools/dfns/make_registry.py rename modflow_devtools/dfns/{schema/v2.py => schema.py} (76%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20292bec..bb44a15f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,8 +124,8 @@ jobs: working-directory: modflow-devtools/autotest env: REPOS_PATH: ${{ github.workspace }} + DFNS_PATH: ${{ github.workspace }}/modflow6/doc/mf6io/mf6ivar/dfn MODFLOW_DEVTOOLS_AUTO_SYNC: 0 - TEST_DFN_PATH: ${{ github.workspace }}/modflow6/doc/mf6io/mf6ivar/dfn # use --dist loadfile to so tests requiring pytest-virtualenv run on the same worker run: uv run pytest -v -n auto --dist loadfile --durations 0 --ignore test_download.py --ignore test_models.py --ignore test_dfns_registry.py diff --git a/autotest/dfn/test_dfn.py b/autotest/dfn/test_dfn.py new file mode 100644 index 00000000..95256c8f --- /dev/null +++ b/autotest/dfn/test_dfn.py @@ -0,0 +1,488 @@ +import dataclasses +from pathlib import Path + +import pytest +from packaging.version import Version + +from modflow_devtools.dfn import Dfn, fetch_dfns +from modflow_devtools.dfn.mapper import map as map_v1_1 +from modflow_devtools.dfn2toml import convert +from modflow_devtools.markers import requires_pkg + +PROJ_ROOT = Path(__file__).parents[1] +DFN_DIR = PROJ_ROOT / "autotest" / "temp" / "dfn" +TOML_V1_DIR = DFN_DIR / "toml" +TOML_V1_1_DIR = DFN_DIR / "toml-v1_1" +VERSIONS = {1: DFN_DIR, 2: TOML_V1_DIR} +MF6_OWNER = "MODFLOW-ORG" +MF6_REPO = "modflow6" +MF6_REF = "develop" +EMPTY_DFNS = {"exg-gwfgwe", "exg-gwfgwt", "exg-gwfprt", "sln-ems"} + + +def pytest_generate_tests(metafunc): + if "dfn_name" in metafunc.fixturenames: + if not any(DFN_DIR.glob("*.dfn")): + fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, DFN_DIR, verbose=True) + dfn_names = [ + dfn.stem for dfn in DFN_DIR.glob("*.dfn") if dfn.stem not in ["common", "flopy"] + ] + metafunc.parametrize("dfn_name", dfn_names, ids=dfn_names) + + if "toml_name" in metafunc.fixturenames: + # Only convert if TOML files don't exist yet (avoid repeated conversions) + dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] + if not TOML_V1_DIR.exists() or not all( + (TOML_V1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths + ): + convert(DFN_DIR, TOML_V1_DIR) + # Verify all expected TOML files were created + assert all((TOML_V1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) + toml_names = [toml.stem for toml in TOML_V1_DIR.glob("*.toml")] + metafunc.parametrize("toml_name", toml_names, ids=toml_names) + + if "toml_v1_1_name" in metafunc.fixturenames: + dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] + if not TOML_V1_1_DIR.exists() or not all( + (TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths + ): + convert(DFN_DIR, TOML_V1_1_DIR, schema_version="1.1") + assert all((TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) + toml_names = [toml.stem for toml in TOML_V1_1_DIR.glob("*.toml")] + metafunc.parametrize("toml_v1_1_name", toml_names, ids=toml_names) + + if "toml_v2_name" in metafunc.fixturenames: + dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] + if not TOML_V2_DIR.exists() or not all( + (TOML_V2_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths + ): + convert(DFN_DIR, TOML_V2_DIR, schema_version="2") + assert all((TOML_V2_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) + toml_names = [toml.stem for toml in TOML_V2_DIR.glob("*.toml")] + metafunc.parametrize("toml_v2_name", toml_names, ids=toml_names) + + +# ============================================================================= +# dfn.v1.Dfn — high-level load +# ============================================================================= + + +@requires_pkg("boltons") +def test_load_v1(dfn_name): + with ( + (DFN_DIR / "common.dfn").open() as common_file, + (DFN_DIR / f"{dfn_name}.dfn").open() as dfn_file, + ): + common, _ = Dfn._load_v1_flat(common_file) + dfn = Dfn.load(dfn_file, name=dfn_name, common=common) + assert any(dfn) + + +@requires_pkg("boltons") +def test_load_v2(toml_name): + with (TOML_V1_DIR / f"{toml_name}.toml").open(mode="rb") as toml_file: + toml = Dfn.load(toml_file, name=toml_name, version=2) + assert any(toml) + + +@requires_pkg("boltons") +@pytest.mark.parametrize("version", list(VERSIONS.keys())) +def test_load_all(version): + dfns = Dfn.load_all(VERSIONS[version], version=version) + assert any(dfns) + + +@requires_pkg("boltons") +def test_convert_v1_1(toml_v1_1_name): + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] + + with (TOML_V1_1_DIR / f"{toml_v1_1_name}.toml").open("rb") as f: + data = tomllib.load(f) + assert data["name"] == toml_v1_1_name + assert data["schema_version"] == "1.1" + + +# ============================================================================= +# DfnSpec (dfn.v1_1.Dfn) — from_dict +# ============================================================================= + + +def test_dfn_from_dict_ignores_extra_keys(): + d = { + "schema_version": Version("2"), + "name": "test-dfn", + "extra_key": "should be allowed", + "another_extra": 123, + } + dfn = DfnSpec.from_dict(d) + assert dfn.name == "test-dfn" + assert dfn.schema_version == Version("2") + + +def test_dfn_from_dict_strict_mode(): + d = { + "schema_version": Version("2"), + "name": "test-dfn", + "extra_key": "should cause error", + } + with pytest.raises(ValueError, match="Unrecognized keys in DFN data"): + DfnSpec.from_dict(d, strict=True) + + +def test_dfn_from_dict_strict_mode_nested(): + d = { + "schema_version": Version("2"), + "name": "test-dfn", + "blocks": { + "options": { + "test_field": { + "name": "test_field", + "type": "keyword", + "extra_key": "should cause error", + }, + }, + }, + } + with pytest.raises(ValueError, match="Unrecognized keys in field data"): + DfnSpec.from_dict(d, strict=True) + + +def test_dfn_from_dict_roundtrip(): + original = DfnSpec( + schema_version=Version("2"), + name="gwf-nam", + parent="sim-nam", + advanced=False, + multi=True, + blocks={"options": {}}, + ) + d = dataclasses.asdict(original) + reconstructed = DfnSpec.from_dict(d) + assert reconstructed.name == original.name + assert reconstructed.schema_version == original.schema_version + assert reconstructed.parent == original.parent + assert reconstructed.advanced == original.advanced + assert reconstructed.multi == original.multi + assert reconstructed.blocks == original.blocks + + +def test_dfn_from_dict_with_v1_field_dicts(): + d = { + "schema_version": Version("1"), + "name": "test-dfn", + "blocks": { + "options": { + "save_flows": { + "name": "save_flows", + "type": "keyword", + "tagged": True, + "in_record": False, + }, + }, + }, + } + dfn = DfnSpec.from_dict(d) + assert dfn.schema_version == Version("1") + assert dfn.name == "test-dfn" + assert dfn.blocks is not None + assert "options" in dfn.blocks + assert "save_flows" in dfn.blocks["options"] + + f = dfn.blocks["options"]["save_flows"] + assert isinstance(f, FieldV1) + assert f.name == "save_flows" + assert f.type == "keyword" + assert f.tagged is True + assert f.in_record is False + + +def test_dfn_from_dict_with_v2_field_dicts(): + d = { + "schema_version": Version("2"), + "name": "test-dfn", + "blocks": { + "dimensions": { + "nper": { + "name": "nper", + "type": "integer", + "optional": False, + }, + }, + }, + } + dfn = DfnSpec.from_dict(d) + assert dfn.schema_version == Version("2") + assert dfn.name == "test-dfn" + assert dfn.blocks is not None + assert "dimensions" in dfn.blocks + assert "nper" in dfn.blocks["dimensions"] + + f = dfn.blocks["dimensions"]["nper"] + assert isinstance(f, Integer) + assert f.name == "nper" + assert f.type == "integer" + assert f.optional is False + + +def test_dfn_from_dict_defaults_to_v2_fields(): + d = { + "name": "test-dfn", + "blocks": { + "options": { + "some_field": { + "name": "some_field", + "type": "keyword", + }, + }, + }, + } + dfn = DfnSpec.from_dict(d) + assert dfn.blocks is not None + f = dfn.blocks["options"]["some_field"] + assert isinstance(f, Keyword) + assert isinstance(f, FieldBase) + assert dfn.schema_version == Version("2") + + +def test_dfn_from_dict_with_already_deserialized_fields(): + kw = Keyword(name="test") + d = { + "schema_version": Version("2"), + "name": "test-dfn", + "blocks": { + "options": { + "test": kw, + }, + }, + } + dfn = DfnSpec.from_dict(d) + assert dfn.blocks is not None + assert dfn.blocks["options"]["test"] is kw + + +# ============================================================================= +# FieldV1 — from_dict +# ============================================================================= + + +def test_fieldv1_from_dict_ignores_extra_keys(): + d = { + "name": "test_field", + "type": "keyword", + "extra_key": "should be allowed", + "another_extra": 123, + } + f = FieldV1.from_dict(d) + assert f.name == "test_field" + assert f.type == "keyword" + + +def test_fieldv1_from_dict_strict_mode(): + d = { + "name": "test_field", + "type": "keyword", + "extra_key": "should cause error", + } + with pytest.raises(ValueError, match="Unrecognized keys in field data"): + FieldV1.from_dict(d, strict=True) + + +def test_fieldv1_from_dict_roundtrip(): + original = FieldV1( + name="maxbound", + type="integer", + block="dimensions", + description="maximum number of cells", + tagged=True, + ) + d = dataclasses.asdict(original) + reconstructed = FieldV1.from_dict(d) + assert reconstructed.name == original.name + assert reconstructed.type == original.type + assert reconstructed.block == original.block + assert reconstructed.description == original.description + assert reconstructed.tagged == original.tagged + + +# ============================================================================= +# map() v1 → v1.1 +# ============================================================================= + + +def test_mapv1to1_1_field_stripping(): + """map(dfn, '1.1') strips v1-specific attrs; shared base attrs are preserved.""" + dfn_v1 = DfnSpec( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "save_flows": FieldV1( + name="save_flows", + type="keyword", + block="options", + description="save calculated flows", + tagged=True, + in_record=False, + reader="urword", + ), + } + }, + ) + + dfn_v1_1 = map_v1_1(dfn_v1, "1.1") + assert dfn_v1_1.schema_version == Version("1.1") + assert dfn_v1_1.blocks is not None + + f = dfn_v1_1.blocks["options"]["save_flows"] + assert isinstance(f, FieldV1_1) + assert not isinstance(f, FieldV1) + assert f.name == "save_flows" + assert f.type == "keyword" + assert f.description == "save calculated flows" + assert f.tagged is True + assert not hasattr(f, "in_record") + assert not hasattr(f, "reader") + + +def test_mapv1to1_1_preserves_dfn_metadata(): + """map(dfn, '1.1') preserves DFN-level metadata (name, parent, advanced, multi).""" + dfn_v1 = DfnSpec( + schema_version=Version("1"), + name="gwf-chd", + parent="gwf-nam", + advanced=False, + multi=True, + blocks={}, + ) + + dfn_v1_1 = map_v1_1(dfn_v1, "1.1") + assert dfn_v1_1.name == "gwf-chd" + assert dfn_v1_1.parent == "gwf-nam" + assert dfn_v1_1.advanced is False + assert dfn_v1_1.multi is True + + +# ============================================================================= +# map() dispatch edge cases +# ============================================================================= + + +def test_map_dispatch_to_v1_raises(): + """map(dfn, '1') raises NotImplementedError.""" + dfn = DfnSpec(schema_version=Version("1"), name="test-dfn") + with pytest.raises(NotImplementedError): + map_v1_1(dfn, "1") + + +def test_map_dispatch_unsupported_version_raises(): + """map(dfn, unsupported version) raises ValueError.""" + dfn = DfnSpec(schema_version=Version("1"), name="test-dfn") + with pytest.raises(ValueError): + map_v1_1(dfn, "3") + + +def test_map_dispatch_already_v1_1_returns_same(): + """map(dfn, '1.1') when dfn is already v1.1 returns the same Dfn unchanged.""" + dfn = DfnSpec(schema_version=Version("1.1"), name="test-dfn") + result = map_v1_1(dfn, "1.1") + assert result is dfn + + +# ============================================================================= +# to_tree / to_flat / _apply_parent_inference +# ============================================================================= + + +def test_apply_parent_inference(): + """_apply_parent_inference infers parents from component names.""" + dfns = { + "sim-nam": DfnSpec(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": DfnSpec(schema_version=Version("1.1"), name="gwf-nam"), + "gwf-dis": DfnSpec(schema_version=Version("1.1"), name="gwf-dis"), + } + inferred = _infer_parents(dfns) + assert inferred["sim-nam"].parent is None + assert inferred["gwf-nam"].parent == "sim-nam" + assert inferred["gwf-dis"].parent == "gwf-nam" + + +def test_apply_parent_inference_does_not_overwrite_explicit(): + """_apply_parent_inference does not overwrite an already-set parent.""" + dfns = { + "gwf-dis": DfnSpec(schema_version=Version("1.1"), name="gwf-dis", parent="custom-parent"), + } + inferred = _infer_parents(dfns) + assert inferred["gwf-dis"].parent == "custom-parent" + + +def test_to_tree_builds_hierarchy(): + """to_tree() builds children hierarchy from a flat Dfns dict.""" + dfns = { + "sim-nam": DfnSpec(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": DfnSpec(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": DfnSpec(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + root = to_tree(dfns) + assert root.name == "sim-nam" + assert root.children is not None + assert "gwf-nam" in root.children + gwf_nam = root.children["gwf-nam"] + assert gwf_nam.children is not None + assert "gwf-dis" in gwf_nam.children + + +def test_to_flat_strips_children(): + """to_flat() recovers the flat spec; no node has children set.""" + dfns = { + "sim-nam": DfnSpec(schema_version=Version("1.1"), name="sim-nam"), + "gwf-nam": DfnSpec(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": DfnSpec(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + root = to_tree(dfns) + flat = to_flat(root) + assert set(flat.keys()) == {"sim-nam", "gwf-nam", "gwf-dis"} + for dfn in flat.values(): + assert dfn.children is None + + +def test_to_tree_raises_without_unique_root(): + """to_tree() raises ValueError when there is no root component.""" + dfns = { + "gwf-nam": DfnSpec(schema_version=Version("1.1"), name="gwf-nam", parent="sim-nam"), + "gwf-dis": DfnSpec(schema_version=Version("1.1"), name="gwf-dis", parent="gwf-nam"), + } + with pytest.raises(ValueError, match="root"): + to_tree(dfns) + + +def test_to_tree_raises_for_v1_schema(): + """to_tree() raises NotImplementedError for v1 schema.""" + dfns = { + "sim-nam": DfnSpec(schema_version=Version("1"), name="sim-nam"), + } + with pytest.raises(NotImplementedError): + to_tree(dfns) + + +def test_block_sort_key_order(): + """block_sort_key orders blocks in canonical MF6 order.""" + from modflow_devtools.dfn.v1_1 import block_sort_key + + items = [ + ("period", {}), + ("options", {}), + ("packagedata", {}), + ("dimensions", {}), + ("custom_block", {}), + ] + sorted_items = sorted(items, key=block_sort_key) + assert [k for k, _ in sorted_items] == [ + "options", + "dimensions", + "packagedata", + "period", + "custom_block", + ] diff --git a/autotest/dfn/test_mapper.py b/autotest/dfn/test_mapper.py new file mode 100644 index 00000000..08caa27b --- /dev/null +++ b/autotest/dfn/test_mapper.py @@ -0,0 +1,97 @@ +def test_toml_safe_primitives_pass_through(): + """_toml_safe passes primitive types through unchanged.""" + assert _toml_safe("hello") == "hello" + assert _toml_safe(42) == 42 + assert _toml_safe(3.14) == 3.14 + assert _toml_safe(True) is True + assert _toml_safe(None) is None + + +def test_toml_safe_non_primitive_coerced_to_str(): + """_toml_safe coerces non-TOML-native types (e.g. Version) to str.""" + assert _toml_safe(Version("1.1")) == "1.1" + + +def test_toml_safe_fieldbase_via_model_dump(): + """_toml_safe converts FieldBase instances via model_dump recursively.""" + kw = Keyword(name="save_flows", description="save flows") + result = _toml_safe(kw) + assert isinstance(result, dict) + assert result["name"] == "save_flows" + assert result["type"] == "keyword" + + +def test_toml_safe_nested(): + """_toml_safe recurses into dicts and lists.""" + obj = {"a": [Version("2"), "plain"], "b": {"c": 99}} + result = _toml_safe(obj) + assert result["a"][0] == "2" + assert result["a"][1] == "plain" + assert result["b"]["c"] == 99 + + +def test_mapper_load_v1(dfn_name): + with ( + (DFN_DIR / "common.dfn").open() as common_file, + (DFN_DIR / f"{dfn_name}.dfn").open() as dfn_file, + ): + common = _load_common(common_file) + dfn = load(dfn_file, name=dfn_name, format="dfn", common=common) + assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) + + +def test_mapper_load_flat(): + dfns = load_flat(path=DFN_DIR) + for dfn in dfns.values(): + assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) + + +def test_dfn_to_plain_dict_version_coerced_and_none_excluded(): + """Version is coerced to str; None fields are excluded from output.""" + dfn = DfnSpec( + schema_version=Version("1.1"), + name="test-dfn", + parent=None, + blocks=None, + ) + d = _dfn_to_plain_dict(dfn) + assert d["schema_version"] == "1.1" + assert d["name"] == "test-dfn" + assert "parent" not in d + assert "blocks" not in d + + +def test_dfn_to_plain_dict_with_fieldbase_blocks(): + """FieldBase blocks are serialized via model_dump.""" + dfn = DfnSpec( + schema_version=Version("2"), + name="test-dfn", + blocks={ + "options": { + "nper": Integer(name="nper", description="number of periods"), + } + }, + ) + d = _dfn_to_plain_dict(dfn) + block = d["blocks"]["options"] + assert "nper" in block + assert block["nper"]["type"] == "integer" + assert block["nper"]["name"] == "nper" + + +def test_dfn_to_plain_dict_with_fieldv1_blocks(): + """FieldV1 blocks are serialized via dataclasses.asdict.""" + dfn = DfnSpec( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "save_flows": FieldV1(name="save_flows", type="keyword", block="options"), + } + }, + ) + d = _dfn_to_plain_dict(dfn) + block = d["blocks"]["options"] + assert "save_flows" in block + assert block["save_flows"]["name"] == "save_flows" + assert block["save_flows"]["type"] == "keyword" diff --git a/autotest/dfns/conftest.py b/autotest/dfns/conftest.py new file mode 100644 index 00000000..5f8f2900 --- /dev/null +++ b/autotest/dfns/conftest.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +import pytest + +from modflow_devtools.dfns import fetch_dfns + +PROJ_ROOT = Path(__file__).parents[1] + +DFNS_REPO = os.getenv("TEST_DFNS_REPO", "MODFLOW-ORG/modflow6") +DFNS_REF = os.getenv("TEST_DFNS_REF", "develop") +DFNS_SOURCE = os.getenv("TEST_DFNS_SOURCE", "modflow6") +DFNS_VERSION = os.getenv("TEST_DFNS_VERSION", "6.6.0") + + +@pytest.fixture(scope="module") +def dfn_dir(module_tmpdir): + """ + Path to DFN files: $DFNS_PATH if set, otherwise fetched from develop branch + to a temp dir (for LocalDfnRegistry tests). + """ + env_var = "DFNS_PATH" + if dfns_path := os.getenv(env_var): + dfn_path = Path(dfns_path).expanduser().resolve() + assert dfn_path.exists(), f"{env_var}={dfns_path} does not exist" + assert any(dfn_path.glob("*.dfn")), f"{env_var}={dfns_path} empty" + return dfn_path + + dfns_path = module_tmpdir / "dfns" + dfns_path.mkdir() + owner = DFNS_REPO.split("/")[0] + repo = DFNS_REPO.split("/")[1] + fetch_dfns(owner, repo, DFNS_REF, dfns_path, verbose=True) + return dfns_path diff --git a/autotest/dfns/test_dfns.py b/autotest/dfns/test_dfns.py new file mode 100644 index 00000000..5937b9b8 --- /dev/null +++ b/autotest/dfns/test_dfns.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from packaging.version import Version + +from modflow_devtools.dfns import Dfns, LocalDfnRegistry +from modflow_devtools.markers import requires_pkg + + +@pytest.mark.parametrize("schema_version", [None, Version("2")]) +def test_load(dfn_dir, schema_version): + spec = Dfns.load(dfn_dir, schema_version=schema_version) + assert spec.schema_version == Version("2") + assert spec.root is not None + assert spec.root.name == "sim-nam" + assert len(spec.components) > 100 + assert "sim-nam" in spec.components + assert "gwf-nam" in spec.components + assert "gwf-chd" in spec.components + assert "gwf-wel" in spec.components.keys() + assert "garbage" not in spec.components + + gwf_chd = spec.components["gwf-chd"] + assert gwf_chd.name == "gwf-chd" + assert gwf_chd.parent == "gwf-nam" + + sim_children = spec.children_of("sim-nam") + assert "gwf-nam" in sim_children + + gwf_children = spec.children_of("gwf-nam") + assert "gwf-chd" in gwf_children + + +def test_load_empty_directory(function_tmpdir): + with pytest.raises(ValueError, match="No DFN files found"): + Dfns.load(function_tmpdir) + + +# ============================================================================= +# Convenience functions with path +# ============================================================================= + + +def test_get_dfn(dfn_dir): + from modflow_devtools.dfns import get_dfn + + dfn = get_dfn("gwf-chd", path=dfn_dir) + assert dfn.name == "gwf-chd" + assert dfn.parent == "gwf-nam" + + +def test_get_dfn_path(dfn_dir): + from modflow_devtools.dfns import get_dfn_path + + file_path = get_dfn_path("gwf-chd", path=dfn_dir) + assert file_path.exists() + assert file_path.name == "gwf-chd.dfn" + + +def test_list_components(dfn_dir): + from modflow_devtools.dfns import list_components + + components = list_components(path=dfn_dir) + assert len(components) > 100 + assert "gwf-chd" in components + + +# ============================================================================= +# Module-level functions +# ============================================================================= + + +@requires_pkg("boltons", "pydantic") +class TestModuleFunctions: + def test_list_components_local(dfn_dir): + registry = LocalDfnRegistry(path=dfn_dir) + components = registry.list_components() + assert len(components) > 100 + assert "gwf-chd" in components + assert "sim-nam" in components + + +# ============================================================================= +# CLI +# ============================================================================= + + +@requires_pkg("pydantic") +class TestCLI: + def test_main_help(self): + from modflow_devtools.dfns.__main__ import main + + result = main([]) + assert result == 0 + + def test_info_command(self): + from modflow_devtools.dfns.__main__ import main + + result = main(["info"]) + assert result == 0 + + def test_clean_command_no_cache(self, tmp_path): + from modflow_devtools.dfns.__main__ import main + + with patch("modflow_devtools.dfns.__main__.get_cache_dir") as mock_cache_dir: + mock_cache_dir.return_value = tmp_path / "nonexistent" + result = main(["clean"]) + + assert result == 0 + + def test_sync_invalid_ref(self): + from modflow_devtools.dfns.__main__ import main + + result = main(["sync", "--ref", "not-a-version"]) + assert result == 1 + + +# ============================================================================= +# Autodiscovery workflow +# ============================================================================= + + +@requires_pkg("boltons", "pydantic") +def test_autodiscovery_workflow(dfn_dir): + from modflow_devtools.dfns import get_dfn, get_registry, list_components + + registry = get_registry(path=dfn_dir, ref="local") + + components = registry.list_components() + assert len(components) > 100 + + gwf_chd = registry.get_dfn("gwf-chd") + assert gwf_chd.name == "gwf-chd" + assert gwf_chd.blocks is not None + + chd_path = registry.get_dfn_path("gwf-chd") + assert chd_path.exists() + + components_list = list_components(path=dfn_dir) + assert "gwf-chd" in components_list + + dfn = get_dfn("gwf-wel", path=dfn_dir) + assert dfn.name == "gwf-wel" diff --git a/autotest/dfns/test_dfns_registry.py b/autotest/dfns/test_dfns_registry.py new file mode 100644 index 00000000..91b66823 --- /dev/null +++ b/autotest/dfns/test_dfns_registry.py @@ -0,0 +1,66 @@ +import flaky +import pytest +from packaging.version import Version + +from modflow_devtools.dfns.registry import LocalDfnRegistry, RemoteDfnRegistry + + +def test_local_dfn_registry(dfn_dir): + registry = LocalDfnRegistry(path=dfn_dir) + assert registry.source == "modflow6" + assert registry.path == dfn_dir.resolve() + + spec = registry.spec + assert spec.schema_version == Version("2") + assert len(spec.components) > 100 + assert "gwf-chd" in spec.components + assert "sim-nam" in spec.components + + dfn = spec.components("gwf-chd") + assert dfn.name == "gwf-chd" + assert dfn.parent == "gwf-nam" + + path = registry.get_path("gwf-chd") + assert path.exists() + assert path.name == "gwf-chd.dfn" + + with pytest.raises(FileNotFoundError, match="nonexistent"): + registry.get_path("nonexistent") + + +def test_remote_dfn_registry_init(): + registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") + assert registry.source == "modflow6" + assert registry.ref == "6.6.0" + + with pytest.raises(ValueError, match="not a valid release version"): + RemoteDfnRegistry(source="modflow6", ref="develop") + + with pytest.raises(ValueError, match="Unknown source"): + RemoteDfnRegistry(source="nonexistent", ref="6.6.0") + + registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") + cache_dir = registry.cache_path() + assert "modflow6" in str(cache_dir) + assert "6.6.0" in str(cache_dir) + + +@pytest.mark.skip(reason="Requires mf{version}_dfns.zip release asset on GitHub") +@flaky(max_runs=3, min_passes=1) +def test_remote_dfn_registry_sync(): + registry = RemoteDfnRegistry(source=DFNS_SOURCE, ref=DFNS_VERSION) + registry.sync(force=True) + + cache_dir = registry.cache_path() + assert cache_dir.exists() + assert any(cache_dir.iterdir()) + + path = registry.get_path("gwf-chd") + assert path.exists() + + spec = registry.spec + assert "gwf-chd" in spec.components + assert "sim-nam" in spec.components + + dfn = spec.components["gwf-chd"] + assert dfn.name == "gwf-chd" diff --git a/autotest/test_dfns_schema.py b/autotest/dfns/test_dfns_schema.py similarity index 57% rename from autotest/test_dfns_schema.py rename to autotest/dfns/test_dfns_schema.py index 80732c83..d10ffd80 100644 --- a/autotest/test_dfns_schema.py +++ b/autotest/dfns/test_dfns_schema.py @@ -1,15 +1,20 @@ -""" -Tests for v2 schema types, DfnSpec dimension resolution, and Array shape validation. -""" +from __future__ import annotations + +import ast import pytest +from modflow_devtools.dfn.v1_1 import Dfn, FieldV1 +from packaging.version import Version -from modflow_devtools.dfns.schema.v2 import ( +from modflow_devtools.dfns import Dfns +from modflow_devtools.dfns.mapper import map as map_v2 +from modflow_devtools.dfns.schema import ( Array, Block, - DfnSpec, Double, + FieldBase, Integer, + Keyword, List, Model, Package, @@ -25,11 +30,8 @@ _validate_sum_call, ) -# ── Helpers ─────────────────────────────────────────────────────────────────── - def _dim_block(*names: str) -> Block: - """Build a dimensions Block with the named Integer dimension fields.""" return Block( name="dimensions", fields={n: Integer(name=n, dimension="component") for n in names}, @@ -40,52 +42,314 @@ def _pkg(name: str, blocks=None, derived_dims=None, parent=None, **kw) -> Packag return Package(name=name, blocks=blocks, derived_dims=derived_dims, parent=parent, **kw) -# ── _collect_explicit_dims ──────────────────────────────────────────────────── +def test_fieldv2_from_dict(): + d = { + "name": "test_field", + "type": "keyword", + "extra_key": "should be allowed", + "another_extra": 123, + } + f = FieldBase.from_dict(d) + assert f.name == "test_field" + assert f.type == "keyword" + assert isinstance(f, Keyword) + + +def test_fieldv2_from_dict_strict(): + d = { + "name": "test_field", + "type": "keyword", + "extra_key": "should cause error", + } + with pytest.raises(ValueError, match="Unrecognized keys in field data"): + FieldBase.from_dict(d, strict=True) + + +def test_fieldv2_from_dict_roundtrip(): + i = Integer( + name="nper", + description="number of stress periods", + optional=False, + ) + d = i.model_dump() + f = FieldBase.from_dict(d) + assert isinstance(f, Integer) + assert f.name == i.name + assert f.type == i.type + assert f.description == i.description + assert f.optional == i.optional + + +def test_map_v2(): + dfn = Dfn(schema_version=Version("2"), name="sim-nam") + result = map_v2(dfn, "2") + assert isinstance(result, Simulation) + + dfn = Dfn(schema_version=Version("2"), name="gwf-nam") + result = map_v2(dfn, "2") + assert isinstance(result, Model) + + dfn = Dfn(schema_version=Version("2"), name="sln-ims") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "solution" + + dfn = Dfn(schema_version=Version("2"), name="exg-gwfgwf") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "exchange" + + dfn = Dfn(schema_version=Version("2"), name="utl-obs") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "utility" + + dfn = Dfn(schema_version=Version("2"), name="gwf-sfr", advanced=True) + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.subtype == "advanced" + + +def test_map_v2_field_conversion(): + dfn = Dfn( + schema_version=Version("2"), + name="gwf-chd", + blocks={ + "options": { + "save_flows": Keyword(name="save_flows", description="save flows"), + } + }, + ) + result = map_v2(dfn, "2") + assert result.name == "gwf-chd" + assert result.blocks is not None + assert "save_flows" in result.blocks["options"].fields + + dfn = Dfn( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "save_flows": FieldV1( + name="save_flows", + type="keyword", + block="options", + description="save calculated flows", + tagged=True, + in_record=False, + reader="urword", + ), + "some_float": FieldV1( + name="some_float", + type="double precision", + block="options", + description="a floating point value", + ), + } + }, + ) + + component = map_v2(dfn, schema_version="2") + assert component.schema_version == Version("2") + assert component.blocks is not None + assert "options" in component.blocks + + options = component.blocks["options"].fields + assert "save_flows" in options + + save_flows = options["save_flows"] + assert isinstance(save_flows, Keyword) + assert isinstance(save_flows, FieldBase) + assert save_flows.name == "save_flows" + assert save_flows.type == "keyword" + assert save_flows.description == "save calculated flows" + assert not hasattr(save_flows, "in_record") + assert not hasattr(save_flows, "reader") + + some_float = options["some_float"] + assert isinstance(some_float, Double) + assert some_float.name == "some_float" + assert some_float.type == "double" + assert some_float.description == "a floating point value" + + +def test_map_v2_period_block_conversion(): + dfn = Dfn( + schema_version=Version("1"), + name="test-pkg", + blocks={ + "period": { + "stress_period_data": FieldV1( + name="stress_period_data", + type="recarray cellid q", + block="period", + description="stress period data", + shape="(maxbound)", + ), + "cellid": FieldV1( + name="cellid", + type="integer", + block="period", + shape="(ncelldim)", + in_record=True, + ), + "q": FieldV1( + name="q", + type="double precision", + block="period", + in_record=True, + ), + } + }, + ) + + component = map_v2(dfn, schema_version="2") + assert component.blocks is not None + for block in component.blocks.values(): + for f in block.fields.values(): + assert isinstance(f, FieldBase) + if f.children: + for child in f.children.values(): + assert isinstance(child, FieldBase) + + period_fields = component.blocks["period"].fields + assert "stress_period_data" in period_fields + spd = period_fields["stress_period_data"] + assert isinstance(spd, List) + assert isinstance(spd.item, Record) + item_fields = spd.item.fields + assert "cellid" in item_fields + assert "q" in item_fields + q = item_fields["q"] + assert isinstance(q, Array) + assert q.dtype == "double" + assert q.shape == ["maxbound"] + + +def test_map_v2_record_conversion(): + """Record type with multiple scalar fields.""" + dfn = Dfn( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "auxrecord": FieldV1( + name="auxrecord", + type="record auxiliary auxname", + block="options", + in_record=False, + ), + "auxiliary": FieldV1( + name="auxiliary", + type="keyword", + block="options", + in_record=True, + ), + "auxname": FieldV1( + name="auxname", + type="string", + block="options", + in_record=True, + ), + } + }, + ) + + component = map_v2(dfn, schema_version="2") + auxrecord = component.blocks["options"].fields["auxrecord"] + assert isinstance(auxrecord, Record) + assert auxrecord.type == "record" + assert auxrecord.children is not None + assert "auxiliary" in auxrecord.children + assert "auxname" in auxrecord.children + assert isinstance(auxrecord.children["auxiliary"], Keyword) + assert isinstance(auxrecord.children["auxname"], String) + + +def test_keystring_type_conversion(): + """Keystring (union) type conversion.""" + dfn = Dfn( + schema_version=Version("1"), + name="test-dfn", + blocks={ + "options": { + "obs_filerecord": FieldV1( + name="obs_filerecord", + type="record obs6 filein obs6_filename", + block="options", + tagged=True, + ), + "obs6": FieldV1( + name="obs6", + type="keyword", + block="options", + in_record=True, + ), + "filein": FieldV1( + name="filein", + type="keyword", + block="options", + in_record=True, + ), + "obs6_filename": FieldV1( + name="obs6_filename", + type="string", + block="options", + in_record=True, + preserve_case=True, + ), + } + }, + ) + component = map_v2(dfn, schema_version="2") + obs_rec = component.blocks["options"].fields["obs_filerecord"] + assert isinstance(obs_rec, Record) + assert obs_rec.type == "record" + assert obs_rec.children is not None + assert all(isinstance(child, FieldBase) for child in obs_rec.children.values()) -def test_collect_explicit_dims_basic(): + +def test_to_component_variant_of(): + dfn = Dfn(schema_version=Version("2"), name="gwf-welg") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.variant_of == "gwf-wel" + + dfn = Dfn(schema_version=Version("2"), name="gwf-rcha") + result = map_v2(dfn, "2") + assert isinstance(result, Package) + assert result.variant_of == "gwf-rch" + + +def test_collect_explicit_dims(): block = _dim_block("nlay", "nrow", "ncol") pkg = _pkg("gwf-dis", blocks={"dimensions": block}) assert _collect_explicit_dims(pkg) == {"nlay", "nrow", "ncol"} - -def test_collect_explicit_dims_ignores_non_dimension_integers(): block = Block( name="options", fields={ "maxbound": Integer(name="maxbound", dimension=False), "nlay": Integer(name="nlay", dimension=True), - }, - ) - pkg = _pkg("test", blocks={"options": block}) - assert _collect_explicit_dims(pkg) == {"nlay"} - - -def test_collect_explicit_dims_ignores_non_integer_fields(): - block = Block( - name="dimensions", - fields={ - "nlay": Integer(name="nlay", dimension=True), "name": String(name="name"), }, ) - pkg = _pkg("test", blocks={"dimensions": block}) + pkg = _pkg("test", blocks={"options": block}) assert _collect_explicit_dims(pkg) == {"nlay"} - -def test_collect_explicit_dims_empty_when_no_blocks(): pkg = _pkg("test", blocks=None) assert _collect_explicit_dims(pkg) == set() - -def test_collect_explicit_dims_across_multiple_blocks(): b1 = Block(name="dimensions", fields={"nlay": Integer(name="nlay", dimension=True)}) b2 = Block(name="griddata", fields={"ncol": Integer(name="ncol", dimension=True)}) pkg = _pkg("test", blocks={"dimensions": b1, "griddata": b2}) assert _collect_explicit_dims(pkg) == {"nlay", "ncol"} - -# ── _names_in_expr ──────────────────────────────────────────────────────────── + b1 = Block(name="dimensions", fields={"nlay": Integer(name="nlay", dimension=True)}) + b2 = Block(name="griddata", fields={"ncol": Integer(name="ncol", dimension=True)}) + pkg = _pkg("test", blocks={"dimensions": b1, "griddata": b2}) + assert _collect_explicit_dims(pkg) == {"nlay", "ncol"} def test_names_in_expr_simple_arithmetic(): @@ -117,12 +381,7 @@ def test_names_in_expr_invalid_syntax(): _names_in_expr("nlay * (") -# ── _validate_sum_call ──────────────────────────────────────────────────────── - - def _make_sum_call(expr: str): - import ast - tree = ast.parse(expr, mode="eval") for node in ast.walk(tree): if isinstance(node, ast.Call): @@ -138,57 +397,44 @@ def _pkg_with_list(list_field_name: str, col_name: str, col_type=None) -> Packag return _pkg("test", blocks={list_field_name: block}) -def test_validate_sum_call_short_form(): +def test_validate_sum_expr(): pkg = _pkg_with_list("packagedata", "nlakeconn") call = _make_sum_call("sum(packagedata.nlakeconn)") _validate_sum_call(call, pkg, "sum(packagedata.nlakeconn)") - -def test_validate_sum_call_long_form(): + # fully qualified pkg = _pkg_with_list("packagedata", "nlakeconn") call = _make_sum_call("sum(packagedata.packagedata.nlakeconn)") _validate_sum_call(call, pkg, "sum(packagedata.packagedata.nlakeconn)") - -def test_validate_sum_call_unknown_list(): + # unrecognized pkg = _pkg("test", blocks=None) call = _make_sum_call("sum(nolist.col)") with pytest.raises(ValueError, match="unknown list field"): _validate_sum_call(call, pkg, "sum(nolist.col)") - -def test_validate_sum_call_wrong_block_qualifier(): pkg = _pkg_with_list("packagedata", "nlakeconn") call = _make_sum_call("sum(wrongblock.packagedata.nlakeconn)") with pytest.raises(ValueError, match="block qualifier"): _validate_sum_call(call, pkg, "sum(wrongblock.packagedata.nlakeconn)") - -def test_validate_sum_call_non_integer_column(): pkg = _pkg_with_list("packagedata", "name", col_type=String) call = _make_sum_call("sum(packagedata.name)") with pytest.raises(ValueError, match="must be Integer"): _validate_sum_call(call, pkg, "sum(packagedata.name)") - -def test_validate_sum_call_missing_column(): pkg = _pkg_with_list("packagedata", "nlakeconn") call = _make_sum_call("sum(packagedata.nosuchcol)") with pytest.raises(ValueError, match="not found"): _validate_sum_call(call, pkg, "sum(packagedata.nosuchcol)") -# ── _resolve_derived_dims ───────────────────────────────────────────────────── - - -def test_resolve_derived_dims_single(): +def test_resolve_derived_dims(): block = _dim_block("nlay", "nrow", "ncol") pkg = _pkg("test", blocks={"dimensions": block}, derived_dims={"nodes": "nlay * nrow * ncol"}) order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) assert order == ["nodes"] - -def test_resolve_derived_dims_chain(): block = _dim_block("nlay", "nrow", "ncol") pkg = _pkg( "test", @@ -198,10 +444,7 @@ def test_resolve_derived_dims_chain(): order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) assert order.index("nodes") < order.index("nodouble") - -def test_resolve_derived_dims_inherited_dim_operand_allowed(): - # A dim from another component (e.g. "nodes" from gwf-dis) is passed in via - # known_dims and should be accepted as a valid derived-dim operand. + # pkg = _pkg("test", blocks=None, derived_dims={"derived": "nodes + 1"}) order = _resolve_derived_dims(pkg, {"nodes"}) assert order == ["derived"] @@ -241,9 +484,6 @@ def test_resolve_derived_dims_invalid_expression_error(): _resolve_derived_dims(pkg, set()) -# ── DfnSpec construction and validation ─────────────────────────────────────── - - def test_dfnspec_construction_validates_dims(): block = _dim_block("nlay", "nrow", "ncol") pkg = _pkg( @@ -251,62 +491,52 @@ def test_dfnspec_construction_validates_dims(): blocks={"dimensions": block}, derived_dims={"nodes": "nlay * nrow * ncol"}, ) - spec = DfnSpec(components={"gwf-dis": pkg}) + spec = Dfns(components={"gwf-dis": pkg}) assert "gwf-dis" in spec.components def test_dfnspec_construction_cycle_raises(): pkg = _pkg("bad", blocks=None, derived_dims={"a": "b + 1", "b": "a + 1"}) with pytest.raises(ValueError, match="Cycle in derived_dims"): - DfnSpec(components={"bad": pkg}) + Dfns(components={"bad": pkg}) def test_dfnspec_construction_unknown_operand_raises(): pkg = _pkg("bad", blocks=None, derived_dims={"nodes": "ghost_dim * 2"}) with pytest.raises(ValueError, match="not a known dimension"): - DfnSpec(components={"bad": pkg}) + Dfns(components={"bad": pkg}) def test_dfnspec_no_derived_dims_constructs_fine(): pkg = _pkg("gwf-chd", blocks=None, derived_dims=None) - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert "gwf-chd" in spec.components -# ── DfnSpec.explicit_dims_for ───────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — DfnSpec.explicit_dims_for +# ============================================================================= def test_dfnspec_explicit_dims_for(): block = _dim_block("nlay", "nrow", "ncol") pkg = _pkg("gwf-dis", blocks={"dimensions": block}) - spec = DfnSpec(components={"gwf-dis": pkg}) + spec = Dfns(components={"gwf-dis": pkg}) assert spec.explicit_dims_for("gwf-dis") == {"nlay", "nrow", "ncol"} def test_dfnspec_explicit_dims_for_empty(): pkg = _pkg("gwf-chd", blocks=None) - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert spec.explicit_dims_for("gwf-chd") == set() -# ── DfnSpec.grid_dims_for ───────────────────────────────────────────────────── - - -def test_dfnspec_grid_dims_for_no_parent_returns_namespace(): - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - pkg = _pkg("sim-nam", blocks=None, parent=None) - spec = DfnSpec(components={"sim-nam": pkg}) - result = spec.grid_dims_for("sim-nam") - assert result == set(GRID_DIM_NAMESPACE) - - def test_dfnspec_grid_dims_for_includes_dis_dims(): dis_block = _dim_block("nlay", "nrow", "ncol") dis = _pkg("gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) grid_dims = spec.grid_dims_for("gwf-chd") assert "nlay" in grid_dims @@ -320,7 +550,7 @@ def test_dfnspec_grid_dims_for_disv(): disv = _pkg("gwf-disv", parent="gwf-nam", blocks={"dimensions": disv_block}) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-disv": disv, "gwf-chd": chd}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-disv": disv, "gwf-chd": chd}) grid_dims = spec.grid_dims_for("gwf-chd") assert "nlay" in grid_dims @@ -332,7 +562,7 @@ def test_dfnspec_grid_dims_for_disu(): disu = _pkg("gwf-disu", parent="gwf-nam", blocks={"dimensions": disu_block}) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-disu": disu, "gwf-chd": chd}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-disu": disu, "gwf-chd": chd}) grid_dims = spec.grid_dims_for("gwf-chd") assert "nodes" in grid_dims @@ -349,61 +579,63 @@ def test_dfnspec_grid_dims_for_non_dis_siblings_excluded(): ) other = _pkg("gwf-chd", parent="gwf-nam", blocks={"dimensions": other_block}) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": other}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": other}) grid_dims = spec.grid_dims_for("gwf-chd") assert "nlay" in grid_dims assert "secret_dim" not in grid_dims -# ── DfnSpec Mapping protocol ────────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — DfnSpec Mapping protocol +# ============================================================================= def test_dfnspec_components_getitem(): pkg = _pkg("gwf-chd", parent="gwf-nam") - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert spec.components["gwf-chd"] is pkg def test_dfnspec_components_iter(): pkg = _pkg("gwf-chd", parent="gwf-nam") - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert list(spec.components) == ["gwf-chd"] def test_dfnspec_components_len(): pkgs = {f"gwf-p{i}": _pkg(f"gwf-p{i}") for i in range(3)} - spec = DfnSpec(components=pkgs) + spec = Dfns(components=pkgs) assert len(spec.components) == 3 def test_dfnspec_components_contains(): pkg = _pkg("gwf-chd") - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert "gwf-chd" in spec.components assert "gwf-rch" not in spec.components -# ── DfnSpec.schema_version ──────────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — DfnSpec.schema_version +# ============================================================================= def test_dfnspec_schema_version_from_component(): - from packaging.version import Version - pkg = Package(name="gwf-chd", schema_version=Version("2")) - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert spec.schema_version == Version("2") def test_dfnspec_schema_version_default(): - from packaging.version import Version - pkg = _pkg("gwf-chd") - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert spec.schema_version == Version("2") -# ── DfnSpec.children_of ─────────────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — DfnSpec.children_of +# ============================================================================= def test_dfnspec_children_of(): @@ -411,29 +643,31 @@ def test_dfnspec_children_of(): chd = _pkg("gwf-chd", parent="gwf-nam") rch = _pkg("gwf-rch", parent="gwf-nam") sim = Simulation(name="sim-nam", blocks=None) - spec = DfnSpec(components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch}) + spec = Dfns(components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch}) children = spec.children_of("gwf-nam") assert set(children) == {"gwf-chd", "gwf-rch"} def test_dfnspec_children_of_empty(): pkg = _pkg("gwf-chd", parent="gwf-nam") - spec = DfnSpec(components={"gwf-chd": pkg}) + spec = Dfns(components={"gwf-chd": pkg}) assert spec.children_of("gwf-chd") == {} -# ── Shape validation helpers ────────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — _known_dims_for +# ============================================================================= -def _dis_spec() -> DfnSpec: +def _dis_spec() -> Dfns: """A minimal gwf-dis + gwf-nam DfnSpec used as shared fixture scaffolding.""" dis_block = _dim_block("nlay", "nrow", "ncol") gwf = Model(name="gwf-nam", blocks=None) dis = Package(name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) - return DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + return Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) -def _lake_spec(period_item: Record) -> DfnSpec: +def _lake_spec(period_item: Record) -> Dfns: """ DfnSpec with a gwf-lak that has a packagedata list block and a period list block whose item is `period_item`. @@ -451,10 +685,7 @@ def _lake_spec(period_item: Record) -> DfnSpec: parent="gwf-nam", blocks={"packagedata": pkg_block, "period": period_block}, ) - return DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) - - -# ── _known_dims_for ─────────────────────────────────────────────────────────── + return Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) def test_known_dims_includes_explicit(): @@ -472,7 +703,7 @@ def test_known_dims_includes_derived(): blocks={"dimensions": dis_block}, derived_dims={"nodes": "nlay * nrow * ncol"}, ) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) known = _known_dims_for(spec, "gwf-dis") assert "nodes" in known @@ -481,13 +712,15 @@ def test_known_dims_includes_grid_dims(): spec = _dis_spec() # gwf-chd has no local dims but inherits grid dims via gwf-dis sibling chd = _pkg("gwf-chd", parent="gwf-nam") - spec2 = DfnSpec(components=dict(spec.components) | {"gwf-chd": chd}) + spec2 = Dfns(components=dict(spec.components) | {"gwf-chd": chd}) known = _known_dims_for(spec2, "gwf-chd") assert "nodes" in known # GRID_DIM_NAMESPACE assert "nlay" in known # from gwf-dis (sibling dis package) -# ── _validate_shape_element: dim reference ──────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — _validate_shape_element: dim reference +# ============================================================================= def _make_ctx(dim_names: set[str], derived: dict | None = None): @@ -495,7 +728,7 @@ def _make_ctx(dim_names: set[str], derived: dict | None = None): dis_block = _dim_block(*dim_names) pkg = _pkg("test", blocks={"dimensions": dis_block}, derived_dims=derived) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "test": pkg}) + spec = Dfns(components={"gwf-nam": gwf, "test": pkg}) known = _known_dims_for(spec, "test") arr = Array(name="arr", dtype="double", shape=[]) return arr, pkg, known @@ -535,7 +768,9 @@ def test_shape_element_empty_string_raises(): _validate_shape_element("", arr, pkg, None, known) -# ── _validate_shape_element: row-level lookup ───────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — _validate_shape_element: row-level lookup +# ============================================================================= def _lookup_ctx(): @@ -546,14 +781,12 @@ def _lookup_ctx(): packagedata block has a List with item Record(lakeno pk, nlakeconn int). The array lives inside a Record with sibling lakeno(fk='packagedata'). """ - # packagedata list nlakeconn = Integer(name="nlakeconn") lakeno_pk = Integer(name="lakeno", pk=True) pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) pkg_list = List(name="packagedata", item=pkg_item) pkg_block = Block(name="packagedata", fields={"packagedata": pkg_list}) - # enclosing record with fk sibling + array fk_lakeno = Integer(name="lakeno", fk="packagedata") arr = Array(name="outflow", dtype="double", shape=[]) enc_record = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) @@ -564,7 +797,7 @@ def _lookup_ctx(): blocks={"packagedata": pkg_block}, ) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) known = _known_dims_for(spec, "gwf-lak") return arr, enc_record, lak, known @@ -593,7 +826,6 @@ def test_shape_element_lookup_unknown_column_raises(): def test_shape_element_lookup_non_integer_column_raises(): - # Replace nlakeconn with a String column nlakeconn = String(name="nlakeconn") lakeno_pk = Integer(name="lakeno", pk=True) pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) @@ -604,7 +836,7 @@ def test_shape_element_lookup_non_integer_column_raises(): enc = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) known = _known_dims_for(spec, "gwf-lak") with pytest.raises(ValueError, match="must be Integer"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) @@ -617,7 +849,6 @@ def test_shape_element_lookup_missing_fk_sibling_raises(): def test_shape_element_lookup_fk_not_set_raises(): - # lakeno has no fk attribute set nlakeconn = Integer(name="nlakeconn") lakeno_pk = Integer(name="lakeno", pk=True) pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) @@ -628,14 +859,13 @@ def test_shape_element_lookup_fk_not_set_raises(): enc = Record(name="item", fields={"lakeno": no_fk_lakeno, "outflow": arr}) lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) known = _known_dims_for(spec, "gwf-lak") with pytest.raises(ValueError, match=r"\.fk is not set"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) def test_shape_element_lookup_fk_block_mismatch_raises(): - # packagedata block exists (check 1 passes) but fk field points to 'otherblock' nlakeconn = Integer(name="nlakeconn") lakeno_pk = Integer(name="lakeno", pk=True) pkg_item = Record(name="item", fields={"lakeno": lakeno_pk, "nlakeconn": nlakeconn}) @@ -646,13 +876,15 @@ def test_shape_element_lookup_fk_block_mismatch_raises(): enc = Record(name="item", fields={"lakeno": fk_lakeno, "outflow": arr}) lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) known = _known_dims_for(spec, "gwf-lak") with pytest.raises(ValueError, match="does not reference block"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) -# ── DfnSpec shape validation end-to-end ────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — DfnSpec shape validation end-to-end +# ============================================================================= def test_dfnspec_valid_top_level_array_shape(): @@ -665,7 +897,7 @@ def test_dfnspec_valid_top_level_array_shape(): blocks={"dimensions": dis_block, "griddata": grid_block}, ) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) assert "gwf-dis" in spec.components @@ -680,7 +912,7 @@ def test_dfnspec_valid_array_in_record(): blocks={"dimensions": dis_block, "options": opt_block}, ) gwf = Model(name="gwf-nam", blocks=None) - DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) def test_dfnspec_valid_row_level_lookup_in_list_item(): @@ -702,7 +934,7 @@ def test_dfnspec_valid_row_level_lookup_in_list_item(): parent="gwf-nam", blocks={"packagedata": pkg_block, "period": period_block}, ) - DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) def test_dfnspec_invalid_array_shape_raises(): @@ -716,7 +948,7 @@ def test_dfnspec_invalid_array_shape_raises(): ) gwf = Model(name="gwf-nam", blocks=None) with pytest.raises(ValueError, match="does not resolve"): - DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) def test_dfnspec_array_shape_resolves_via_derived_dim(): @@ -730,7 +962,7 @@ def test_dfnspec_array_shape_resolves_via_derived_dim(): derived_dims={"nodes": "nlay * nrow * ncol"}, ) gwf = Model(name="gwf-nam", blocks=None) - DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis}) + Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) def test_dfnspec_array_shape_resolves_via_sibling_dis(): @@ -741,10 +973,12 @@ def test_dfnspec_array_shape_resolves_via_sibling_dis(): chd_block = Block(name="period", fields={"head": chd_arr}) chd = Package(name="gwf-chd", parent="gwf-nam", blocks={"period": chd_block}) gwf = Model(name="gwf-nam", blocks=None) - DfnSpec(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) + Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) -# ── _validate_fk_fields ─────────────────────────────────────────────────────── +# ============================================================================= +# dfns.schema.v2 — _validate_fk_fields +# ============================================================================= def _fk_pkg_and_spec(fk_val, pk_on_item=True, fk_ref=None): @@ -774,32 +1008,32 @@ def _fk_pkg_and_spec(fk_val, pk_on_item=True, fk_ref=None): def test_validate_fk_fields_valid(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) assert "gwf-lak" in spec.components def test_validate_fk_fields_unknown_block_raises(): lak, gwf = _fk_pkg_and_spec("nosuchblock", pk_on_item=True) with pytest.raises(ValueError, match="is not a list block"): - DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) def test_validate_fk_fields_no_pk_on_item_raises(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=False) with pytest.raises(ValueError, match="has no pk=True field"): - DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) def test_validate_fk_fields_fk_ref_valid(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True, fk_ref="gwf-nam") - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) assert "gwf-lak" in spec.components def test_validate_fk_fields_fk_ref_unknown_raises(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True, fk_ref="no-such-comp") with pytest.raises(ValueError, match="not found in spec"): - DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) def test_validate_fk_fields_no_fk_set_passes(): @@ -808,11 +1042,143 @@ def test_validate_fk_fields_no_fk_set_passes(): block = Block(name="data", fields={"data": lst}) pkg = Package(name="gwf-test", blocks={"data": block}) gwf = Model(name="gwf-nam", blocks=None) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-test": pkg}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-test": pkg}) assert "gwf-test" in spec.components def test_validate_fk_fields_called_directly(): lak, gwf = _fk_pkg_and_spec("packagedata", pk_on_item=True) - spec = DfnSpec(components={"gwf-nam": gwf, "gwf-lak": lak}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) _validate_fk_fields(lak, spec) # should not raise + + +# ============================================================================= +# dfns.schema.v2 — Block.optional +# ============================================================================= + + +def test_block_optional_all_optional_fields(): + from modflow_devtools.dfns.schema import Keyword + + block = Block( + name="options", + fields={ + "verbose": Keyword(name="verbose", optional=True), + "maxiter": Integer(name="maxiter", optional=True), + }, + ) + assert block.optional is True + + +def test_block_optional_has_required_field(): + block = Block( + name="dimensions", + fields={ + "nlay": Integer(name="nlay"), + "nrow": Integer(name="nrow", optional=True), + }, + ) + assert block.optional is False + + +def test_block_optional_empty_fields(): + block = Block(name="empty", fields={}) + assert block.optional is True + + +# ============================================================================= +# dfns.schema.v2 — Array shape position-based rules +# ============================================================================= + + +def test_top_level_array_empty_shape_valid(): + arr = Array(name="auxiliary", dtype="string", shape=[]) + block = Block(name="options", fields={"auxiliary": arr}) + pkg = Package(name="gwf-test", blocks={"options": block}) + gwf = Model(name="gwf-nam", blocks=None) + Dfns(components={"gwf-nam": gwf, "gwf-test": pkg}) + + +def test_non_rightmost_inline_array_empty_shape_raises(): + arr = Array(name="vals", dtype="double", shape=[]) + extra = Integer(name="extra") + rec = Record(name="myrec", fields={"vals": arr, "extra": extra}) + block = Block(name="data", fields={"myrec": rec}) + pkg = Package(name="gwf-test", blocks={"data": block}) + gwf = Model(name="gwf-nam", blocks=None) + with pytest.raises(ValueError, match="rightmost"): + Dfns(components={"gwf-nam": gwf, "gwf-test": pkg}) + + +def test_rightmost_inline_array_empty_shape_valid(): + arr = Array(name="auxvals", dtype="double", shape=[]) + rec = Record(name="myrec", fields={"auxvals": arr}) + block = Block(name="data", fields={"myrec": rec}) + pkg = Package(name="gwf-test", blocks={"data": block}) + gwf = Model(name="gwf-nam", blocks=None) + Dfns(components={"gwf-nam": gwf, "gwf-test": pkg}) + + +def test_rightmost_inline_string_array_empty_shape_valid(): + arr = Array(name="auxname", dtype="string", shape=[]) + rec = Record(name="aux_rec", fields={"auxname": arr}) + block = Block(name="options", fields={"aux_rec": rec}) + pkg = Package(name="gwf-test", blocks={"options": block}) + gwf = Model(name="gwf-nam", blocks=None) + Dfns(components={"gwf-nam": gwf, "gwf-test": pkg}) + + +# ============================================================================= +# dfns.schema.v2 — DfnSpec schema version consistency +# ============================================================================= + + +def test_dfnspec_schema_version_consistency_raises(): + pkg1 = Package(name="gwf-chd", schema_version=Version("2")) + pkg2 = Package(name="gwf-wel", schema_version=Version("3")) + with pytest.raises(ValueError, match="schema_version"): + Dfns(components={"gwf-chd": pkg1, "gwf-wel": pkg2}) + + +def test_dfnspec_schema_version_consistency_null_ignored(): + pkg1 = Package(name="gwf-chd", schema_version=Version("2")) + pkg2 = Package(name="gwf-wel", schema_version=None) + spec = Dfns(components={"gwf-chd": pkg1, "gwf-wel": pkg2}) + assert spec.schema_version == Version("2") + + +# ============================================================================= +# dfns.schema.v2 — Bound-annotated shape elements +# ============================================================================= + + +def test_shape_element_bound_lt(): + arr, pkg, known = _make_ctx({"nrow"}) + _validate_shape_element("nrow", arr, pkg, None, known) + + +def test_shape_element_bound_lte(): + arr, pkg, known = _make_ctx({"ncol"}) + _validate_shape_element("<=ncol", arr, pkg, None, known) + + +def test_shape_element_bound_gte(): + arr, pkg, known = _make_ctx({"ncol"}) + _validate_shape_element(">=ncol", arr, pkg, None, known) + + +def test_shape_element_bound_unknown_dim_raises(): + arr, pkg, known = _make_ctx({"nlay"}) + with pytest.raises(ValueError, match="does not resolve"): + _validate_shape_element(" 100 # Should have many components - - # Test components iteration - names = list(spec.components) - assert "sim-nam" in names - assert "gwf-nam" in names - assert "gwf-chd" in names - - # Test components access - gwf_chd = spec.components["gwf-chd"] - assert gwf_chd.name == "gwf-chd" - assert gwf_chd.parent == "gwf-nam" - - # Test components containment - assert "gwf-chd" in spec.components - assert "nonexistent" not in spec.components - - # Test components keys(), values(), items() - assert "gwf-wel" in spec.components.keys() - assert any(d.name == "gwf-wel" for d in spec.components.values()) - assert any(n == "gwf-wel" for n, d in spec.components.items()) - - def test_getitem_raises_keyerror(self, dfn_dir): - """Test that __getitem__ raises KeyError for missing components.""" - from modflow_devtools.dfns import DfnSpec - - spec = DfnSpec.load(dfn_dir) - - with pytest.raises(KeyError, match="nonexistent"): - _ = spec.components["nonexistent"] - - def test_hierarchical_access(self, dfn_dir): - """Test accessing components through the hierarchical tree.""" - from modflow_devtools.dfns import DfnSpec - - spec = DfnSpec.load(dfn_dir) - - # Root should be sim-nam - assert spec.root.name == "sim-nam" - - # children_of is the query API; components carry parent, not children - root_children = spec.children_of("sim-nam") - assert "gwf-nam" in root_children - - gwf_nam_children = spec.children_of("gwf-nam") - assert "gwf-chd" in gwf_nam_children - - def test_load_empty_directory_raises(self, tmp_path): - """Test that loading from empty directory raises ValueError.""" - from modflow_devtools.dfns import DfnSpec - - with pytest.raises(ValueError, match="No DFN files found"): - DfnSpec.load(tmp_path) - - -@requires_pkg("pydantic") -class TestBootstrapConfig: - """Tests for bootstrap configuration schemas.""" - - def test_source_config_defaults(self): - """Test SourceConfig default values.""" - from modflow_devtools.dfns.registry import SourceConfig - - config = SourceConfig(repo="owner/repo") - - assert config.repo == "owner/repo" - assert config.dfn_path == "doc/mf6io/mf6ivar/dfn" - assert config.registry_path == ".registry/dfns.toml" - assert config.refs == [] - - def test_source_config_custom_values(self): - """Test SourceConfig with custom values.""" - from modflow_devtools.dfns.registry import SourceConfig - - config = SourceConfig( - repo="custom/repo", - dfn_path="custom/path", - registry_path="custom/registry.toml", - refs=["main", "v1.0"], - ) - - assert config.repo == "custom/repo" - assert config.dfn_path == "custom/path" - assert config.registry_path == "custom/registry.toml" - assert config.refs == ["main", "v1.0"] - - def test_bootstrap_config_load(self, tmp_path): - """Test loading BootstrapConfig from TOML file.""" - from modflow_devtools.dfns.registry import BootstrapConfig - - config_file = tmp_path / "dfns.toml" - config_file.write_text(""" -[sources.test] -repo = "test/repo" -refs = ["main"] -""") - - config = BootstrapConfig.load(config_file) - - assert "test" in config.sources - assert config.sources["test"].repo == "test/repo" - assert config.sources["test"].refs == ["main"] - - def test_bootstrap_config_load_nonexistent(self, tmp_path): - """Test loading from nonexistent file returns empty config.""" - from modflow_devtools.dfns.registry import BootstrapConfig - - config = BootstrapConfig.load(tmp_path / "nonexistent.toml") - - assert config.sources == {} - - def test_bootstrap_config_merge(self): - """Test merging two bootstrap configs.""" - from modflow_devtools.dfns.registry import BootstrapConfig, SourceConfig - - base = BootstrapConfig( - sources={ - "source1": SourceConfig(repo="base/source1", refs=["v1"]), - "source2": SourceConfig(repo="base/source2"), - } - ) - overlay = BootstrapConfig( - sources={ - "source1": SourceConfig(repo="overlay/source1", refs=["v2"]), - "source3": SourceConfig(repo="overlay/source3"), - } - ) - - merged = BootstrapConfig.merge(base, overlay) - - # overlay overrides base for source1 - assert merged.sources["source1"].repo == "overlay/source1" - assert merged.sources["source1"].refs == ["v2"] - # source2 from base preserved - assert merged.sources["source2"].repo == "base/source2" - # source3 from overlay added - assert merged.sources["source3"].repo == "overlay/source3" - - def test_get_bootstrap_config(self): - """Test loading bundled bootstrap config.""" - from modflow_devtools.dfns.registry import get_bootstrap_config - - config = get_bootstrap_config() - - assert "modflow6" in config.sources - assert config.sources["modflow6"].repo == "MODFLOW-ORG/modflow6" - - -@requires_pkg("pydantic") -class TestRegistryMeta: - """Tests for registry metadata schemas.""" - - def test_dfn_registry_meta_load(self, tmp_path): - """Test loading DfnRegistryMeta from TOML file.""" - from modflow_devtools.dfns.registry import DfnRegistryMeta - - registry_file = tmp_path / "dfns.toml" - registry_file.write_text(""" -schema_version = "1.0" - -[metadata] -ref = "6.6.0" - -[files."gwf-chd.dfn"] -hash = "sha256:abc123" - -[files."gwf-wel.dfn"] -hash = "sha256:def456" -""") - - meta = DfnRegistryMeta.load(registry_file) - - assert meta.schema_version == "1.0" - assert meta.ref == "6.6.0" - assert len(meta.files) == 2 - assert meta.files["gwf-chd.dfn"].hash == "sha256:abc123" - assert meta.files["gwf-wel.dfn"].hash == "sha256:def456" - - def test_dfn_registry_meta_save(self, tmp_path): - """Test saving DfnRegistryMeta to TOML file.""" - import tomli - - from modflow_devtools.dfns.registry import DfnRegistryFile, DfnRegistryMeta - - meta = DfnRegistryMeta( - schema_version="1.0", - ref="test-ref", - files={ - "test.dfn": DfnRegistryFile(hash="sha256:abc123"), - }, - ) - - output_path = tmp_path / "output.toml" - meta.save(output_path) - - assert output_path.exists() - - with output_path.open("rb") as f: - data = tomli.load(f) - - assert data["schema_version"] == "1.0" - assert data["metadata"]["ref"] == "test-ref" - assert data["files"]["test.dfn"]["hash"] == "sha256:abc123" - - -@requires_pkg("boltons", "pydantic") -class TestLocalDfnRegistry: - """Tests for LocalDfnRegistry class.""" - - def test_init(self, dfn_dir): - """Test LocalDfnRegistry initialization.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir, ref="local") - - assert registry.source == "modflow6" - assert registry.ref == "local" - assert registry.path == dfn_dir.resolve() - - def test_spec_property(self, dfn_dir): - """Test accessing spec through registry.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - spec = registry.spec - - assert spec.schema_version == Version("2") - assert len(spec) > 100 - - def test_get_dfn(self, dfn_dir): - """Test getting a DFN by name.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - dfn = registry.get_dfn("gwf-chd") - - assert dfn.name == "gwf-chd" - assert dfn.parent == "gwf-nam" - - def test_get_dfn_path(self, dfn_dir): - """Test getting file path for a component.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - path = registry.get_dfn_path("gwf-chd") - - assert path.exists() - assert path.name == "gwf-chd.dfn" - - def test_get_dfn_path_not_found(self, dfn_dir): - """Test getting path for nonexistent component raises FileNotFoundError.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - with pytest.raises(FileNotFoundError, match="nonexistent"): - registry.get_dfn_path("nonexistent") - - def test_schema_version_property(self, dfn_dir): - """Test schema_version property.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - assert registry.schema_version == Version("2") - - def test_components_property(self, dfn_dir): - """Test components property returns flat dict.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - - components = registry.components - - assert isinstance(components, dict) - assert "gwf-chd" in components - assert components["gwf-chd"].name == "gwf-chd" - - -@requires_pkg("pydantic") -class TestCacheUtilities: - """Tests for cache and config utilities.""" - - def test_get_cache_dir(self): - """Test getting cache directory path.""" - from modflow_devtools.dfns.registry import get_cache_dir - - cache_dir = get_cache_dir("dfn") - - assert cache_dir.name == "dfn" - assert "modflow-devtools" in str(cache_dir) - - def test_get_user_config_path(self): - """Test getting user config path.""" - from modflow_devtools.dfns.registry import get_user_config_path - - config_path = get_user_config_path("dfn") - - assert config_path.name == "dfns.toml" - assert "modflow-devtools" in str(config_path) - - def test_get_cache_dir_custom_subdir(self): - """Test cache dir with custom subdirectory.""" - from modflow_devtools.dfns.registry import get_cache_dir - - cache_dir = get_cache_dir("custom") - - assert cache_dir.name == "custom" - - -@requires_pkg("tomli", "tomli_w") -class TestMakeRegistry: - """Tests for the registry generation tool.""" - - def test_compute_file_hash(self, tmp_path): - """Test computing file hash.""" - from modflow_devtools.dfns.make_registry import compute_file_hash - - test_file = tmp_path / "test.txt" - test_file.write_text("hello world") - - hash_value = compute_file_hash(test_file) - - assert hash_value.startswith("sha256:") - # Known hash for "hello world" - assert "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" in hash_value - - def test_scan_dfn_directory(self, dfn_dir): - """Test scanning a DFN directory.""" - from modflow_devtools.dfns.make_registry import scan_dfn_directory - - files = scan_dfn_directory(dfn_dir) - - assert len(files) > 100 - assert "gwf-chd.dfn" in files - assert "common.dfn" in files - assert all(h.startswith("sha256:") for h in files.values()) - - def test_generate_registry(self, dfn_dir, tmp_path): - """Test generating a registry file.""" - import tomli - - from modflow_devtools.dfns.make_registry import generate_registry - - output_path = tmp_path / "dfns.toml" - - generate_registry( - dfn_path=dfn_dir, - output_path=output_path, - ref="test-ref", - ) - - assert output_path.exists() - - with output_path.open("rb") as f: - data = tomli.load(f) - - assert data["schema_version"] == "1.0" - assert "generated_at" in data - assert data["metadata"]["ref"] == "test-ref" - assert "gwf-chd.dfn" in data["files"] - - def test_generate_registry_empty_dir(self, tmp_path): - """Test generating registry from empty directory raises ValueError.""" - from modflow_devtools.dfns.make_registry import generate_registry - - with pytest.raises(ValueError, match="No DFN files found"): - generate_registry( - dfn_path=tmp_path, - output_path=tmp_path / "dfns.toml", - ) - - def test_cli_help(self): - """Test CLI help output.""" - from modflow_devtools.dfns.make_registry import main - - # --help should exit with 0 - with pytest.raises(SystemExit) as exc_info: - main(["--help"]) - assert exc_info.value.code == 0 - - def test_cli_generate(self, dfn_dir, tmp_path): - """Test CLI generate command.""" - from modflow_devtools.dfns.make_registry import main - - output_path = tmp_path / "dfns.toml" - - result = main( - [ - "--dfn-path", - str(dfn_dir), - "--output", - str(output_path), - "--ref", - "test-ref", - ] - ) - - assert result == 0 - assert output_path.exists() - - -@requires_pkg("pydantic") -class TestCLI: - """Tests for the DFNs CLI.""" - - def test_main_help(self): - """Test CLI help output.""" - from modflow_devtools.dfns.__main__ import main - - result = main([]) - assert result == 0 - - def test_info_command(self): - """Test info command.""" - from modflow_devtools.dfns.__main__ import main - - result = main(["info"]) - assert result == 0 - - def test_clean_command_no_cache(self, tmp_path): - """Test clean command when cache doesn't exist.""" - from modflow_devtools.dfns.__main__ import main - - # Patch get_cache_dir to return nonexistent directory - with patch("modflow_devtools.dfns.__main__.get_cache_dir") as mock_cache_dir: - mock_cache_dir.return_value = tmp_path / "nonexistent" - result = main(["clean"]) - - assert result == 0 - - def test_sync_command_no_registry(self): - """Test sync command when registry doesn't exist (expected to fail).""" - from modflow_devtools.dfns.__main__ import main - - # This should fail because MODFLOW 6 repo doesn't have the registry yet - result = main(["sync", "--ref", "nonexistent-ref"]) - assert result == 1 - - -@requires_pkg("pydantic", "pooch", "boltons") -class TestRemoteDfnRegistry: - """Tests for RemoteDfnRegistry with mocked network calls.""" - - def test_init(self): - """Test RemoteDfnRegistry initialization.""" - from modflow_devtools.dfns import RemoteDfnRegistry - - registry = RemoteDfnRegistry(source="modflow6", ref="develop") - - assert registry.source == "modflow6" - assert registry.ref == "develop" - - def test_unknown_source_raises(self): - """Test that unknown source raises ValueError.""" - from modflow_devtools.dfns import RemoteDfnRegistry - - with pytest.raises(ValueError, match="Unknown source"): - RemoteDfnRegistry(source="nonexistent", ref="develop") - - def test_construct_raw_url(self): - """Test URL construction.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - - url = registry._construct_raw_url("doc/mf6io/mf6ivar/dfn") - - assert "raw.githubusercontent.com" in url - assert "MODFLOW-ORG/modflow6" in url - assert "6.6.0" in url - - def test_get_registry_cache_path(self): - """Test getting registry cache path.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - - path = registry._get_registry_cache_path() - - assert "registries" in str(path) - assert "modflow6" in str(path) - assert "6.6.0" in str(path) - assert path.name == "dfns.toml" - - def test_get_files_cache_dir(self): - """Test getting files cache directory.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - - path = registry._get_files_cache_dir() - - assert "files" in str(path) - assert "modflow6" in str(path) - assert "6.6.0" in str(path) - - def test_fetch_registry_not_found(self): - """Test that fetching nonexistent registry raises appropriate error.""" - from modflow_devtools.dfns.registry import ( - DfnRegistryNotFoundError, - RemoteDfnRegistry, - ) - - registry = RemoteDfnRegistry(source="modflow6", ref="nonexistent-ref-12345") - - with pytest.raises(DfnRegistryNotFoundError): - registry._fetch_registry(force=True) - - def test_init_with_repo_override(self): - """Test RemoteDfnRegistry with repo override.""" - from modflow_devtools.dfns import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - assert registry.source == TEST_DFN_SOURCE - assert registry.ref == TEST_DFN_REF - assert registry.repo == TEST_DFN_REPO - - def test_construct_raw_url_with_repo_override(self): - """Test URL construction with repo override.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - url = registry._construct_raw_url("doc/mf6io/mf6ivar/dfn") - - assert "raw.githubusercontent.com" in url - assert TEST_DFN_REPO in url - assert TEST_DFN_REF in url - - @flaky(max_runs=3, min_passes=1) - def test_fetch_registry(self): - """Test fetching registry from the test repository.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - meta = registry._fetch_registry(force=True) - - assert meta is not None - assert len(meta.files) > 0 - # Registry file may have a different ref than what we requested - # (e.g., generated from develop branch but accessed on registry branch) - assert meta.ref is not None - - @flaky(max_runs=3, min_passes=1) - def test_sync_files(self): - """Test syncing DFN files from the test repository.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - # Sync should succeed (fetches registry and sets up pooch) - registry.sync(force=True) - - # Should be able to fetch a DFN file - path = registry.get_dfn_path("gwf-chd") - assert path.exists() - - @flaky(max_runs=3, min_passes=1) - def test_get_dfn(self): - """Test getting a DFN from the test repository.""" - from modflow_devtools.dfns import Dfn - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - # Ensure synced - registry.sync() - - dfn = registry.get_dfn("gwf-chd") - - assert isinstance(dfn, Dfn) - assert dfn.name == "gwf-chd" - - @flaky(max_runs=3, min_passes=1) - def test_get_spec(self): - """Test getting the full spec from the test repository.""" - from modflow_devtools.dfns import DfnSpec - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - # Ensure synced - registry.sync() - - spec = registry.spec - - assert isinstance(spec, DfnSpec) - assert "gwf-chd" in spec - assert "sim-nam" in spec - - @flaky(max_runs=3, min_passes=1) - def test_list_components(self): - """Test listing available components from the test repository.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry - - registry = RemoteDfnRegistry( - source=TEST_DFN_SOURCE, - ref=TEST_DFN_REF, - repo=TEST_DFN_REPO, - ) - - # Ensure synced - registry.sync() - - # Use spec.keys() to list components - components = list(registry.spec.keys()) - - assert len(components) > 100 - assert "gwf-chd" in components - assert "sim-nam" in components - - -@requires_pkg("boltons", "pydantic") -class TestModuleFunctions: - """Tests for module-level convenience functions.""" - - def test_list_components_local(self, dfn_dir): - """Test list_components with local registry.""" - from modflow_devtools.dfns import LocalDfnRegistry - - registry = LocalDfnRegistry(path=dfn_dir) - components = list(registry.spec.keys()) - - assert len(components) > 100 - assert "gwf-chd" in components - assert "sim-nam" in components - - def test_get_sync_status(self): - """Test get_sync_status function.""" - from modflow_devtools.dfns.registry import get_sync_status - - status = get_sync_status() - - assert isinstance(status, dict) - # All refs should be either True or False - assert all(isinstance(v, bool) for v in status.values()) - - -@requires_pkg("boltons", "pydantic") -class TestGetRegistryWithPath: - """Tests for get_registry() with path parameter.""" - - def test_get_registry_with_path_returns_local_registry(self, dfn_dir): - """Test that get_registry with path returns LocalDfnRegistry.""" - from modflow_devtools.dfns.registry import LocalDfnRegistry, get_registry - - registry = get_registry(path=dfn_dir) - - assert isinstance(registry, LocalDfnRegistry) - assert registry.path == dfn_dir.resolve() - - def test_get_registry_with_path_and_metadata(self, dfn_dir): - """Test that source/ref metadata is preserved with path.""" - from modflow_devtools.dfns.registry import get_registry - - registry = get_registry(path=dfn_dir, source="test", ref="local") - - assert registry.source == "test" - assert registry.ref == "local" - - def test_get_registry_without_path_returns_remote_registry(self): - """Test that get_registry without path still returns RemoteDfnRegistry.""" - from modflow_devtools.dfns.registry import RemoteDfnRegistry, get_registry - - registry = get_registry(source="modflow6", ref="develop", auto_sync=False) - - assert isinstance(registry, RemoteDfnRegistry) - - -@requires_pkg("boltons", "pydantic") -class TestConvenienceFunctionsWithPath: - """Tests for convenience functions with path parameter.""" - - def test_get_dfn_with_path(self, dfn_dir): - """Test get_dfn() with path parameter.""" - from modflow_devtools.dfns import get_dfn - - dfn = get_dfn("gwf-chd", path=dfn_dir) - - assert dfn.name == "gwf-chd" - assert dfn.parent == "gwf-nam" - - def test_get_dfn_path_with_path(self, dfn_dir): - """Test get_dfn_path() with path parameter.""" - from modflow_devtools.dfns import get_dfn_path - - file_path = get_dfn_path("gwf-chd", path=dfn_dir) - - assert file_path.exists() - assert file_path.name == "gwf-chd.dfn" - - def test_list_components_with_path(self, dfn_dir): - """Test list_components() with path parameter.""" - from modflow_devtools.dfns import list_components - - components = list_components(path=dfn_dir) - - assert len(components) > 100 - assert "gwf-chd" in components - - -@requires_pkg("boltons", "pydantic") -def test_autodiscovery_workflow(dfn_dir): - """Test complete autodiscovery workflow.""" - from modflow_devtools.dfns import get_dfn, get_registry, list_components - - # Get registry pointing at local directory - registry = get_registry(path=dfn_dir, ref="local") - - # List components - components = list(registry.spec.keys()) - assert len(components) > 100 - - # Get specific DFN - gwf_chd = registry.get_dfn("gwf-chd") - assert gwf_chd.name == "gwf-chd" - assert gwf_chd.blocks is not None - - # Get file path - chd_path = registry.get_dfn_path("gwf-chd") - assert chd_path.exists() - - # Use convenience functions - components_list = list_components(path=dfn_dir) - assert "gwf-chd" in components_list - - dfn = get_dfn("gwf-wel", path=dfn_dir) - assert dfn.name == "gwf-wel" diff --git a/autotest/test_models.py b/autotest/test_models.py index f54b1c72..54658773 100644 --- a/autotest/test_models.py +++ b/autotest/test_models.py @@ -17,8 +17,8 @@ DiscoveredModelRegistry, ModelRegistry, ModelRegistryDiscoveryError, - ModelSourceConfig, - ModelSourceRepo, + ModelSource, + ModelSources, get_user_config_path, ) @@ -33,19 +33,19 @@ class TestBootstrap: def test_load_bootstrap(self): """Test loading the bootstrap file.""" - bootstrap = ModelSourceConfig.load() - assert isinstance(bootstrap, ModelSourceConfig) + bootstrap = ModelSources.load() + assert isinstance(bootstrap, ModelSources) assert len(bootstrap.sources) > 0 def test_bootstrap_has_testmodels(self): """Test that testmodels is configured.""" - bootstrap = ModelSourceConfig.load() + bootstrap = ModelSources.load() assert TEST_MODELS_SOURCE in bootstrap.sources def test_bootstrap_testmodels_config(self): """Test testmodels configuration in bundled config (without user overlay).""" bundled_path = Path(__file__).parent.parent / "modflow_devtools" / "models" / "models.toml" - bootstrap = ModelSourceConfig.load(bootstrap_path=bundled_path) + bootstrap = ModelSources.load(bootstrap_path=bundled_path) testmodels = bootstrap.sources[TEST_MODELS_SOURCE] assert "MODFLOW-ORG/modflow6-testmodels" in testmodels.repo @@ -53,7 +53,7 @@ def test_bootstrap_testmodels_config(self): def test_bootstrap_source_has_name(self): """Test that bootstrap sources have name injected.""" - bootstrap = ModelSourceConfig.load() + bootstrap = ModelSources.load() for key, source in bootstrap.sources.items(): assert source.name is not None # If no explicit name override, name should equal key @@ -72,25 +72,23 @@ def test_get_user_config_path(self): def test_merge_bootstrap(self): """Test merging bundled and user bootstrap configs.""" # Create bundled config - bundled = ModelSourceConfig( + bundled = ModelSources( sources={ - "source1": ModelSourceRepo(repo="org/repo1", name="source1", refs=["main"]), - "source2": ModelSourceRepo(repo="org/repo2", name="source2", refs=["develop"]), + "source1": ModelSource(repo="org/repo1", name="source1", refs=["main"]), + "source2": ModelSource(repo="org/repo2", name="source2", refs=["develop"]), } ) # Create user config that overrides source1 and adds source3 - user = ModelSourceConfig( + user = ModelSources( sources={ - "source1": ModelSourceRepo( - repo="user/custom-repo1", name="source1", refs=["feature"] - ), - "source3": ModelSourceRepo(repo="user/repo3", name="source3", refs=["master"]), + "source1": ModelSource(repo="user/custom-repo1", name="source1", refs=["feature"]), + "source3": ModelSource(repo="user/repo3", name="source3", refs=["master"]), } ) # Merge - merged = ModelSourceConfig.merge(bundled, user) + merged = ModelSources.merge(bundled, user) # Check that user source1 overrode bundled source1 assert merged.sources["source1"].repo == "user/custom-repo1" @@ -121,7 +119,7 @@ def test_load_bootstrap_with_user_config(self, tmp_path): ) # Load bootstrap with user config path specified - bootstrap = ModelSourceConfig.load(user_config_path=user_config) + bootstrap = ModelSources.load(user_config_path=user_config) # Check that user config was merged assert "custom-models" in bootstrap.sources @@ -154,7 +152,7 @@ def test_load_bootstrap_explicit_path_no_overlay(self, tmp_path): ) # Load with explicit path only (no user_config_path) - bootstrap = ModelSourceConfig.load(explicit_config) + bootstrap = ModelSources.load(explicit_config) # Should only have explicit source, not user source assert "explicit-source" in bootstrap.sources @@ -183,9 +181,7 @@ def test_load_bootstrap_explicit_path_with_overlay(self, tmp_path): ) # Load with both explicit paths - bootstrap = ModelSourceConfig.load( - bootstrap_path=explicit_config, user_config_path=user_config - ) + bootstrap = ModelSources.load(bootstrap_path=explicit_config, user_config_path=user_config) # Should have both sources assert "explicit-source" in bootstrap.sources @@ -199,7 +195,7 @@ class TestBootstrapSourceMethods: def test_source_has_sync_method(self): """Test that ModelSourceRepo has sync method.""" - bootstrap = ModelSourceConfig.load() + bootstrap = ModelSources.load() source = bootstrap.sources[TEST_MODELS_SOURCE] assert hasattr(source, "sync") assert callable(source.sync) @@ -236,7 +232,7 @@ class TestDiscovery: def test_discover_registry(self): """Test discovering registry for test repo.""" # Use test repo/ref from environment - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -253,7 +249,7 @@ def test_discover_registry(self): @flaky(max_runs=3, min_passes=1) def test_discover_registry_nonexistent_ref(self): """Test that discovery fails gracefully for nonexistent ref.""" - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=["nonexistent-branch-12345"], @@ -272,7 +268,7 @@ def test_sync_single_source_single_ref(self): """Test syncing a single source/ref.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -290,7 +286,7 @@ def test_sync_creates_cache(self): _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) assert not _DEFAULT_CACHE.has(TEST_MODELS_SOURCE_NAME, TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -303,7 +299,7 @@ def test_sync_skip_cached(self): """Test that sync skips already-cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -323,7 +319,7 @@ def test_sync_force(self): """Test that force flag re-syncs cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -344,7 +340,7 @@ def test_sync_via_source_method(self): _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) # Create source with test repo override - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -361,7 +357,7 @@ def test_source_is_synced_method(self): """Test ModelSourceRepo.is_synced() method.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -376,7 +372,7 @@ def test_source_list_synced_refs_method(self): """Test ModelSourceRepo.list_synced_refs() method.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -395,7 +391,7 @@ class TestRegistry: def synced_registry(self): """Fixture that syncs and loads a registry once for all tests.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -463,7 +459,7 @@ def test_cli_list_empty(self, capsys): def test_cli_list_with_cache(self, capsys): """Test 'list' command with cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -490,7 +486,7 @@ def test_cli_clear(self, capsys): """Test 'clear' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -519,7 +515,7 @@ def test_cli_copy(self, tmp_path): """Test 'copy' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -556,7 +552,7 @@ def test_cli_copy_nonexistent_model(self, tmp_path, capsys): """Test 'copy' command with nonexistent model.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -589,7 +585,7 @@ def test_cli_cp_alias(self, tmp_path): """Test 'cp' alias for 'copy' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -627,7 +623,7 @@ def test_python_cp_alias(self, tmp_path): """Test Python API cp() alias for copy_to().""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -666,7 +662,7 @@ def test_full_workflow(self): """Test complete workflow: discover -> cache -> load.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -689,7 +685,7 @@ def test_sync_and_list_models(self): """Test syncing and listing available models.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSourceRepo( + source = ModelSource( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], diff --git a/docs/md/dfns.md b/docs/md/dfns.md index e10c1d9b..9a3ae5b0 100644 --- a/docs/md/dfns.md +++ b/docs/md/dfns.md @@ -43,7 +43,7 @@ The tool may also be used on individual files. To validate legacy format files, > **Note**: This module is experimental. The API may change without following normal deprecation procedures. -The `modflow_devtools.dfns` module provides a richer API for working with MODFLOW 6 input specifications, including structured Python objects, a registry system for remote discovery and caching, and serialization to a single TOML document. +The `modflow_devtools.dfns` module provides a richer API for working with MODFLOW 6 input specifications, including structured Python objects, a registry system for remote discovery and caching, and serialization to TOML. ### Formats @@ -62,7 +62,7 @@ Both formats are supported by `modflow_devtools.dfns`. The v2 schema (TOML) is t Represents a single MODFLOW 6 input component (e.g. `gwf-chd`, `sim-nam`). A dataclass with attributes including `name`, `schema_version`, `blocks`, `parent`, `advanced`, `multi`, `subcomponents`, and optionally `children` (when part of a tree). ```python -from modflow_devtools.dfns import load +from modflow_devtools.dfns import DfnSpec # Load a single component from a TOML file with open("gwf-chd.toml", "rb") as f: @@ -103,11 +103,11 @@ toml_str = spec.dumps() ### Registry -The registry system handles discovering, caching, and accessing DFN files from remote sources (primarily the MODFLOW 6 GitHub repository). +The registry system handles discovering, caching, and accessing DFN files from MODFLOW 6 releases. Only released versions are supported by `RemoteDfnRegistry`; for working with unreleased or local DFN files, use `LocalDfnRegistry`. #### `LocalDfnRegistry` -For working with DFN files on the local filesystem: +For working with DFN files on the local filesystem. This is the right choice when working with a local MODFLOW 6 checkout, a CI environment with DFN files checked out, or any directory of DFN files not associated with a published release. ```python from modflow_devtools.dfns import LocalDfnRegistry @@ -119,13 +119,13 @@ spec = registry.spec #### `RemoteDfnRegistry` -For fetching and caching DFN files from a remote source. Uses [Pooch](https://www.fatiando.org/pooch/) for caching and hash verification. +For fetching and caching DFN files from a MODFLOW 6 release. On first access for a given version, downloads the `mf{version}_dfns.zip` release asset from GitHub, extracts it to a local cache directory, and uses it for all subsequent access. Only accepts released version strings (e.g. `"6.6.0"`), not branch names or arbitrary git refs. ```python from modflow_devtools.dfns import RemoteDfnRegistry registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") -registry.sync() # downloads and caches the registry + DFN files +registry.sync() # downloads and caches DFN files for 6.6.0 dfn = registry.get_dfn("gwf-chd") spec = registry.spec @@ -134,12 +134,22 @@ spec = registry.spec #### Convenience functions ```python -from modflow_devtools.dfns import get_dfn, get_dfn_path, get_registry, list_components, sync_dfns - -# Sync all configured refs +from modflow_devtools.dfns import ( + get_dfn, + get_dfn_path, + get_registry, + list_components, + list_releases, + sync_dfns, +) + +# List available releases +releases = list_releases() # e.g. ["6.6.0", "6.5.0", "6.4.4"] + +# Sync all available releases sync_dfns() -# Sync a specific ref +# Sync a specific release sync_dfns(ref="6.6.0") # Get a component (auto-syncs if MODFLOW_DEVTOOLS_AUTO_SYNC=1) @@ -148,13 +158,13 @@ dfn = get_dfn("gwf-chd", ref="6.6.0") # Get the local cached path to a component file path = get_dfn_path("gwf-wel", ref="6.6.0") -# List all components for a ref +# List all components for a release components = list_components(ref="6.6.0") -# Get a registry object +# Get a registry object for a release registry = get_registry(ref="6.6.0") -# Use a local path instead of remote +# Use a local path instead of a remote release registry = get_registry(path="/path/to/dfns") dfn = get_dfn("gwf-chd", path="/path/to/dfns") ``` @@ -162,10 +172,13 @@ dfn = get_dfn("gwf-chd", path="/path/to/dfns") #### CLI ```shell -# Sync all configured refs +# List available releases +python -m modflow_devtools.dfns releases + +# Sync all available releases python -m modflow_devtools.dfns sync -# Sync a specific ref +# Sync a specific release python -m modflow_devtools.dfns sync --ref 6.6.0 # Force re-download @@ -174,7 +187,7 @@ python -m modflow_devtools.dfns sync --force # Show sync status and cache info python -m modflow_devtools.dfns info -# List available components for a ref +# List available components for a release python -m modflow_devtools.dfns list --ref 6.6.0 # Clear cache @@ -190,24 +203,23 @@ Auto-sync is opt-in (off by default). Enable it by setting the environment varia MODFLOW_DEVTOOLS_AUTO_SYNC=1 ``` -When enabled, `get_registry()` will automatically sync if no cached registry exists for the requested ref. +When enabled, `get_registry()` will automatically sync if no cached files exist for the requested release. #### Cache location -Cached registries and DFN files are stored under: +Downloaded DFN files are cached under: ``` -~/.cache/modflow-devtools/dfn/ -├── registries/ -│ └── modflow6/ -│ └── 6.6.0/ -│ └── dfns.toml -└── files/ - └── modflow6/ - └── 6.6.0/ - ├── sim-nam.toml - ├── gwf-chd.toml - └── ... +~/.cache/modflow-devtools/dfns/ +└── modflow6/ + ├── 6.6.0/ + │ ├── sim-nam.toml + │ ├── gwf-chd.toml + │ └── ... + └── 6.5.0/ + ├── sim-nam.dfn + ├── gwf-chd.dfn + └── ... ``` ### Schema versioning and mapping diff --git a/docs/md/dfn-schema.md b/docs/md/dfnspec.md similarity index 86% rename from docs/md/dfn-schema.md rename to docs/md/dfnspec.md index aec32044..989dd4fe 100644 --- a/docs/md/dfn-schema.md +++ b/docs/md/dfnspec.md @@ -17,13 +17,11 @@ - [Type-specific attributes](#type-specific-attributes-1) - [`multi`](#multi) - [`subtype`](#subtype) - - [`variant_of`](#variant_of) - [Blocks](#blocks-1) - [Attributes](#attributes) - [`name`](#name-1) - [`fields`](#fields) - [`repeats`](#repeats) - - [`optional`](#optional) - [Fields](#fields-1) - [Shared attributes](#shared-attributes-1) - [`name`](#name-2) @@ -34,11 +32,11 @@ - [`default`](#default) - [`developmode`](#developmode) - [`netcdf`](#netcdf) + - [`tagged`](#tagged) - [Scalars](#scalars) - [Keyword](#keyword) - [String](#string) - [Type-specific attributes](#type-specific-attributes-2) - - [`tagged`](#tagged) - [`valid`](#valid) - [`case_sensitive`](#case_sensitive) - [`pk`](#pk) @@ -46,7 +44,6 @@ - [`fk_ref`](#fk_ref) - [Integer](#integer) - [Type-specific attributes](#type-specific-attributes-3) - - [`tagged`](#tagged-1) - [`valid`](#valid-1) - [`dimension`](#dimension) - [`time_series`](#time_series) @@ -55,7 +52,6 @@ - [`fk_ref`](#fk_ref-1) - [Double](#double) - [Type-specific attributes](#type-specific-attributes-4) - - [`tagged`](#tagged-2) - [`time_series`](#time_series-1) - [Path](#path) - [Type-specific attributes](#type-specific-attributes-5) @@ -136,7 +132,7 @@ Parent relationships are defined bottom-up with attribute `parent`: #### `schema_version` -`string | null (default: null)`. The version of the DFN schema. Optional but recommended. +`string | null (default: null)`. The version of the DFN schema. Optional but recommended. When multiple components are loaded together into a `DfnSpec`, all non-null `schema_version` values must agree; mixed versions are a validation error. ### Component types @@ -177,16 +173,10 @@ Optional discriminator indicating the package's functional role. Several package 1. Solves a continuity equation. Each feature (well, reach, lake cell, UZF cell) internally balances inflows, outflows, and change in storage. Traditional stress packages impose static conditions and do not have an internal water budget. **Note:** advanced packages can act as receivers in the Water Mover (MVR/MVE) package because they have an internal continuity equation to receive diverted water into. Traditional stress packages cannot. 2. Has dynamic state variables. Advanced packages compute a dependent variable (e.g., lake stage, well head, reach stage) that is part of the solution. Traditional stress packages use fixed/user-specified values. 3. Stress periods have feature replacement rather than block replacement semantics: when a new period block configuration is provided, traditional stress packages replace the entire previous configuration; advanced packages perform partial updates, modifying only features explicitly appearing in the new period block. **Note:** both simple and advanced packages fill-forward across omitted stress periods; the distinction is only in what happens when a new period block configuration is specified. -- `"utility"`: an auxiliary package that may be attached to models or packages, such as time series, time-array series, or observations. Utility packages (`utl-*`) are distinguished from primary model input packages by providing configurational or cross-cutting concerns rather than representing a first-class hydrologic process. They may support `multi` and `variant_of`; they never have `subtype` `"solution"`, `"exchange"`, `"stress"`, or `"advanced"`. +- `"utility"`: an auxiliary package that may be attached to models or packages, such as time series, time-array series, or observations. Utility packages (`utl-*`) are distinguished from primary model input packages by providing configurational or cross-cutting concerns rather than representing a first-class hydrologic process. They may support `multi`. `subtype: null` (the default) covers packages that don't fall into any named category, such as output control packages. -###### `variant_of` - -`string | null (default: null)`. Some modules may be represented by several different components: e.g., typical stress packages define period data sparsely as a list, while layer- and grid-array variants allow providing period data as arrays. - -A package may signal that it has equivalent functional semantics as another component with the `variant_of` attribute. This attribute is only meaningful on packages (including utility packages); variants of models or simulations are not supported. - ## Blocks A block is group of related fields, essentially a product type. Record fields are also product types; the distinction is that records occupy a single line in MF6 input files, while blocks are multiline constructs delimited by headers, e.g. @@ -214,13 +204,20 @@ A field's value need not be preceded by its name; see the `tagged` section below #### `repeats` -`boolean (default: false)`. Whether the block may appear multiple times in an input file. When true, each occurrence is read independently, and associated with a unique label. The canonical repeating block is the period block, whose label is the stress period number. +`boolean (default: false)`. Whether the block may appear multiple times in an input file. When true, each occurrence is read independently and associated with a unique label. The canonical repeating block is the period block, whose label is the stress period number. + +A block has no explicit `optional` attribute. Its optionality is derived from its fields: a block is optional if and only if all of its fields are optional (vacuously true for an empty block). This combines with `repeats` to give four configurations: -**Note:** if a repeating block contains any required fields, it must appear at least once. If a repeating block contains only optional fields, it can appear zero or more times. +| `repeats` | derived optional | Meaning | +|---|---|---| +| `false` | `false` | must appear exactly once | +| `false` | `true` | may appear at most once | +| `true` | `false` | must appear at least once | +| `true` | `true` | may appear zero or more times | ## Fields -A field is a [tagged union](https://en.wikipedia.org/wiki/Tagged_union) of concrete data types, discriminated by a `type` attribute. A field consists of a set of attributes, some shared, some type-specific. +A field is a [union](https://en.wikipedia.org/wiki/Tagged_union) of concrete data types, discriminated by a `type` attribute. A field consists of a set of attributes, some shared, some type-specific. Field definitions are not entirely self-contained. Some fields may refer to other fields, in the same component or in another. There are two cases of this: @@ -241,6 +238,7 @@ There is a core set of attributes shared by all field types: - `default` - `developmode` - `netcdf` +- `tagged` #### `name` @@ -284,6 +282,10 @@ The field's default value. Only relevant for optional fields. TODO: determine wh `boolean (default: false)`. Marks a field that can appear in NetCDF input files. +#### `tagged` + +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. + ### Scalars Scalar fields define a single value. @@ -298,10 +300,6 @@ Type `string`. ##### Type-specific attributes -###### `tagged` - -`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. - ###### `valid` `[string] | null`. Permitted values (enumeration constraint). Empty list is treated as absent. @@ -330,7 +328,7 @@ Type `integer`. ###### `tagged` -`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. ###### `valid` @@ -364,7 +362,7 @@ Type `double`. ###### `tagged` -`boolean (default: true)`. Indicates that the field value should be preceded by the field name. Valid only for record subfields. All other scalar fields are necessarily tagged. +`boolean (default: true)`. Indicates that the field value should be preceded by the field name. ###### `time_series` @@ -392,6 +390,8 @@ Type `array`. Arrays are not proper composites. An array does not have an item subfield as does a list. Instead, it has a `dtype` attribute identifying its scalar element type. An array may not contain composite elements; `dtype` must be a scalar type. +An array is **self-sizing** when its `shape` is empty (`[]`). A self-sizing array is read inline by the parser: it consumes tokens until the end of the current line, dynamically determining its own length. Its element count may serve as a dimension for other arrays (see `dimension` below). The only invalid position for a self-sizing array is as a non-rightmost subfield of a record, where subsequent fields on the same line would be unreadable. Arrays with a declared shape are parsed to exactly that many elements. + ##### Type-specific attributes ###### `dtype` @@ -400,9 +400,13 @@ Arrays are not proper composites. An array does not have an item subfield as doe ###### `shape` -`[string]`. The array's shape. Each element is a shape expression — either a global dimension name (explicit or derived; see [Array dimensions](#array-dimensions)) or a row-level column lookup (see [Row-level column lookups](#row-level-column-lookups)). The latter form is only valid when the array is a subfield of a record. +`[string] (default: [])`. The array's shape, as a list of shape expressions. An empty list means the array is **self-sizing** (see above). There is one constraint on self-sizing arrays: they may not appear as a non-rightmost subfield of a record, because subsequent fields on the same line would be unreadable. In all other positions — top-level in a block, or rightmost in a record — a self-sizing array is valid. Parsing rules by position: + +- **Top-level** (a direct field of a block, not inside a record): read as inline tokens on the block line. `shape` may be empty (self-sizing) or declared. +- **Inline, not rightmost** (a subfield of a record with at least one subsequent field): `shape` must be declared and non-empty; the size must be determinable from already-parsed context (a global dim or a preceding sibling field). +- **Inline, rightmost** (the last subfield of a record): `shape` may be empty (self-sizing) or declared. -For `dtype: "string"` arrays, `shape` must be empty (`[]`). String arrays are read inline and self-sizing; no shape expression is needed or meaningful. Shape expressions on string arrays are validation errors. +Each declared shape expression is either a global dimension name (explicit or derived; see [Array dimensions](#array-dimensions)) or a row-level column lookup (see [Row-level column lookups](#row-level-column-lookups)). The latter form is only valid when the array is a subfield of a record. Any shape expression may additionally carry an advisory **bound annotation** prefix (`<`, `>`, `<=`, or `>=`); the bound is not enforced by the MF6 parser. ###### `time_series` @@ -414,7 +418,9 @@ For `dtype: "string"` arrays, `shape` must be empty (`[]`). String arrays are re ###### `dimension` -`"component" | "model" | "simulation" | null (default: null)`. Valid only when `dtype` is `"string"`. Marks this array as a named dimension source at the given scope: the array's name may appear in other arrays' `shape` expressions to mean "one value per element of this string array." Because string arrays are always read inline (not via READARRAY), the MF6 parser counts tokens on the fly; no numeric value needs to be declared in advance. `shape` must be empty for any array with `dimension` set. Not meaningful on non-string arrays; `"record"` scope is not valid for arrays. +`"component" | "model" | "simulation" | null (default: null)`. Marks this self-sizing array as a dimension source at the given scope: the array's name may appear in other arrays' `shape` expressions to mean "one value per element of this array." Valid only on self-sizing arrays (`shape` must be empty); a schema error on arrays with a declared shape. + +Because a self-sizing array is read inline and dynamically sized, the parser knows its element count immediately after reading the line — no separate integer field is needed to declare the size in advance. That count is what other arrays reference when they name this array in their `shape`. The dtype of the dimension-source array is not constrained: a string array whose elements are named identifiers (e.g. auxiliary variable names) and a numeric array whose elements happen to fix a count both provide the same thing to the shape system — a dynamic integer size. `"record"` scope is not valid for arrays. #### Record @@ -574,7 +580,7 @@ connectiondata: Validation rules: - Valid only when the array is a subfield of a record (not a top-level block field). -- The sibling field must be an `integer` or any `array`. No `dimension: true` annotation is required — intra-record sibling resolution is purely structural; `dimension: true` is only needed to register a field in the global dim scope. +- The sibling field must be an `integer` with `dimension: "record"`. The `"record"` scope annotation makes the role explicit and prevents accidental resolution of unrelated integer fields. - Resolution order: the scope chain is tried first; sibling resolution is only the fallback when the identifier does not resolve globally. #### Bound-annotated shape expressions @@ -592,7 +598,7 @@ sfacval: Dim references (plain identifiers) resolve in this order: -1. Local explicit dims: `integer` fields with `dimension` set and `array` fields with `dtype: "string"` and `dimension` set, in this component +1. Local explicit dims: `integer` fields with `dimension` set and self-sizing `array` fields (i.e. `shape: []`) with `dimension` set, in this component 2. Local derived dims: entries in this component's `derived_dims`, resolved in dependency order 3. Inherited dims: explicit dims from other components in the spec (filtered by scope — `"model"` dims available to packages in the same model, `"simulation"` dims available to all) 4. Intra-record siblings with `dimension: "record"`: a sibling `integer` in the same enclosing record that has been explicitly marked as a per-row inline count — fallback when steps 1–3 all fail and the array is inside a record. @@ -601,14 +607,14 @@ Row-level column lookups and bound annotations (` FieldV1_1: - return FieldV1_1( - name=field.name, - type=field.type, - block=field.block, - default=field.default, - longname=field.longname, - description=field.description, - optional=field.optional, - developmode=field.developmode, - shape=field.shape, - valid=field.valid, - netcdf=field.netcdf, - tagged=field.tagged, - ) - - def map(self, dfn: Dfn) -> Dfn: - blocks: dict[str, dict] = {} - for block_name, block_fields in (dfn.blocks or {}).items(): - blocks[block_name] = { - fname: MapV1To1_1.map_field(f) - for fname, f in block_fields.items() - if isinstance(f, FieldV1) - } - return dataclasses.replace( - dfn, - schema_version=Version("1.1"), - blocks=blocks if blocks else None, - ) - - -def map( - dfn: Dfn, - schema_version: str | Version = "1.1", -) -> Dfn: - """Map a MODFLOW 6 v1 definition to v1 or v1.1 schema.""" - version = Version(str(schema_version)) - if version == Version("1"): - raise NotImplementedError("Mapping to schema version 1 is not implemented.") - if version == Version("1.1"): - if dfn.schema_version >= Version("1.1"): - return dfn - return MapV1To1_1().map(dfn) - raise ValueError(f"Unsupported schema version: {schema_version!r}. Expected '1' or '1.1'.") - - -# ============================================================================= -# I/O helpers -# ============================================================================= - - -def load(f: Any, format: str = "dfn", **kwargs: Any) -> Dfn: - """Load a MODFLOW 6 definition file into a Dfn.""" - if format == "dfn": - name = kwargs.pop("name") - fields_parsed, meta = parse_dfn(f, **kwargs) - blocks = { - block_name: {field_dict["name"]: FieldV1.from_dict(field_dict) for field_dict in block} - for block_name, block in groupby( - fields_parsed.values(multi=True), lambda fd: fd["block"] - ) - } - subcomponents = parse_mf6_subpackages(meta) - return Dfn( - name=name, - schema_version=Version("1"), - parent=try_parse_parent(meta), - advanced=is_advanced_package(meta), - multi=is_multi_package(meta), - blocks=blocks, - subcomponents=subcomponents if subcomponents else None, - ) - - if format == "toml": - from modflow_devtools.dfns.schema.v2 import FieldBase - - data = tomli.load(f) - dfn_name = data.pop("name", kwargs.pop("name", None)) - - dfn_fields: dict[str, Any] = { - "name": dfn_name, - "schema_version": Version(str(data.pop("schema_version", "2"))), - "parent": data.pop("parent", None), - "advanced": data.pop("advanced", False), - "multi": data.pop("multi", False), - "ftype": data.pop("ftype", None), - } - # variant_of is a v2 Component concept; consume but don't store on Dfn - data.pop("variant_of", None) - - if (expected_name := kwargs.pop("name", None)) is not None: - if dfn_fields["name"] != expected_name: - raise ValueError(f"DFN name mismatch: {expected_name} != {dfn_fields['name']}") - - parsed_blocks: dict[str, Any] = {} - for section_name, section_data in data.items(): - if isinstance(section_data, dict): - block_fields: dict[str, Any] = {} - for field_name, field_data in section_data.items(): - if isinstance(field_data, dict): - block_fields[field_name] = FieldBase.from_dict(field_data) - else: - block_fields[field_name] = field_data - parsed_blocks[section_name] = block_fields - - dfn_fields["blocks"] = parsed_blocks if parsed_blocks else None - return Dfn(**dfn_fields) - - raise ValueError(f"Unsupported format: {format!r}. Expected 'dfn' or 'toml'.") +from modflow_devtools.dfn import schema as v1 -def _load_common(f: Any) -> Any: - common, _ = parse_dfn(f) - return common - - -def load_flat(path: str | PathLike) -> Dfns: +def map_field(field: v1.Field) -> v1.Field: """ - Load a flat MODFLOW 6 specification from definition files in a directory. - - Returns a dictionary of unlinked Dfns (children not populated). + Map a component field definition from the v1 schema to v1.1. + This simply """ - exclude = ["common", "flopy"] - path = Path(path).expanduser().resolve() - - dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.stem not in exclude} - toml_paths = {p.stem: p for p in path.glob("*.toml") if p.stem not in exclude} - dfns: Dfns = {} - if dfn_paths: - with (path / "common.dfn").open() as f: - common = _load_common(f) - for dfn_name, dfn_path in dfn_paths.items(): - with dfn_path.open() as f: - dfns[dfn_name] = load(f, name=dfn_name, common=common, format="dfn") - if toml_paths: - for toml_name, toml_path in toml_paths.items(): - with toml_path.open("rb") as f: - dfns[toml_name] = load(f, name=toml_name, format="toml") - return dfns - - -def _infer_parent(name: str) -> str | None: - """Infer the parent component name from a component name using MF6 conventions.""" - if name == "sim-nam": - return None - if name.endswith("-nam"): - return "sim-nam" - if name.startswith(("exg-", "sln-", "utl-")): - return "sim-nam" - if "-" in name: - mdl = name.split("-")[0] - return f"{mdl}-nam" - return None - - -def _resolve_parent_for_tree(name: str, parent: str | list[str] | None, dfns: Dfns) -> str | None: - """ - Resolve a parent value to a specific component name for tree placement. - - When parent is a type label or any string not present in the known component - dict, falls back to name-based inference. - """ - if parent is None: - return None - if isinstance(parent, str) and parent in dfns: - return parent - return _infer_parent(name) - - -def _apply_parent_inference(dfns: Dfns) -> Dfns: - """Set parent on any Dfn where it is not already explicit.""" - result: Dfns = {} - for name, dfn in dfns.items(): - if dfn.parent is None: - inferred = _infer_parent(name) - result[name] = dataclasses.replace(dfn, parent=inferred) if inferred else dfn - else: - result[name] = dfn - return result - - -def to_tree(dfns: Dfns) -> Dfn: - """ - Infer the MODFLOW 6 input component hierarchy from a flat spec. - - Returns the root component. There must be exactly one root (no parent). - """ - dfns = _apply_parent_inference(dfns) - first_dfn = next(iter(dfns.values()), None) - - match schema_version := str(first_dfn.schema_version if first_dfn else Version("1")): - case "1": - raise NotImplementedError("Tree inference from v1 schema not implemented") - case "1.1" | "2": - roots = {name: dfn for name, dfn in dfns.items() if dfn.parent is None} - if (nroots := len(roots)) != 1: - raise ValueError(f"Expected one root component, found {nroots}") - - def _build_tree(node_name: str) -> Dfn: - node = dfns[node_name] - children = { - name: dfn - for name, dfn in dfns.items() - if _resolve_parent_for_tree(name, dfn.parent, dfns) == node_name - } - if children: - node = dataclasses.replace( - node, - children={name: _build_tree(name) for name in children}, - ) - return node - - return _build_tree(next(iter(roots.keys()))) - case _: - raise ValueError( - f"Unsupported schema version: {schema_version!r}. Expected '1.1' or '2'." - ) - - -def to_flat(dfn: Dfn) -> Dfns: - """Flatten a MODFLOW 6 input component hierarchy to a flat spec.""" - - def _flatten(d: Dfn) -> Dfns: - result: Dfns = {d.name: dataclasses.replace(d, children=None)} - for child in (d.children or {}).values(): - result.update(_flatten(child)) - return result - - return _flatten(dfn) - - -def is_valid(path: str | PathLike, format: str = "dfn", verbose: bool = False) -> bool: - """Validate DFN file(s).""" - path = Path(path).expanduser().absolute() - try: - if not path.exists(): - raise FileNotFoundError(f"Path does not exist: {path}") - - if path.is_file(): - common: Any = {} - if (common_path := path.parent / "common.dfn").exists(): - with common_path.open() as f: - common, _ = parse_dfn(f) - if path.name == "common.dfn": - return True - with path.open() as f: - load(f, name=path.stem, common=common, format=format) - else: - load_flat(path) - return True - except Exception as e: - if verbose: - print(f"Validation failed: {e}") - return False - - -# ============================================================================= -# Serialization helpers -# ============================================================================= - - -def _dfn_to_plain_dict(dfn: Dfn) -> dict: - """Serialize a Dfn (dataclass) to a plain Python dict.""" - from modflow_devtools.dfns.schema.v2 import FieldBase - - d: dict[str, Any] = {} - for field_name in dfn.__dataclass_fields__: - v = getattr(dfn, field_name) - if v is None: - continue - if isinstance(v, Version): - d[field_name] = str(v) - else: - d[field_name] = v - - if blocks := d.get("blocks"): - serialized: dict[str, dict] = {} - for block_name, block_fields in blocks.items(): - block_out: dict = {} - for field_name, field_val in block_fields.items(): - if isinstance(field_val, FieldBase): - block_out[field_name] = field_val.model_dump(exclude_none=True) - elif dataclasses.is_dataclass(field_val) and not isinstance(field_val, type): - block_out[field_name] = asdict(field_val) - else: - block_out[field_name] = field_val - serialized[block_name] = block_out - d["blocks"] = serialized - - return d + return v1.Field( + name=field["name"], + type=field["type"], + block=field["block"], + default=field["default"], + longname=field["longname"], + description=field["description"], + optional=field["optional"], + developmode=field["developmode"], + shape=field["shape"], + valid=field["valid"], + netcdf=field["netcdf"], + tagged=field["tagged"], + ) + + +def map(dfn: v1.Dfn) -> v1.Dfn: + """Map a component definition from the v1 schema to v1.1.""" + + blocks: dict[str, dict] = {} + for block_name, block_fields in (dfn["blocks"] or {}).items(): + blocks[block_name] = { + field_name: map_field(field) + for field_name, field in block_fields.items() + if isinstance(field, v1.Field) + } -def _toml_safe(obj: Any) -> Any: - """Recursively coerce non-TOML-native types to str.""" - from modflow_devtools.dfns.schema.v2 import FieldBase + for block_name, block_fields in blocks.items(): + dfn.setdefault(block_name, {}) + for field_name, field_data in block_fields.items(): + dfn[block_name][field_name] = field_data - if isinstance(obj, FieldBase): - return _toml_safe(obj.model_dump(exclude_none=True)) - if isinstance(obj, dict): - return {k: _toml_safe(v) for k, v in obj.items()} - if isinstance(obj, list): - return [_toml_safe(v) for v in obj] - if isinstance(obj, (str, int, float, bool)) or obj is None: - return obj - return str(obj) + dfn["blocks"] = blocks if blocks else None + dfn["schema_version"] = "1.1" + return dfn diff --git a/modflow_devtools/dfn/parse.py b/modflow_devtools/dfn/parser.py similarity index 98% rename from modflow_devtools/dfn/parse.py rename to modflow_devtools/dfn/parser.py index ca25219b..0eb397dc 100644 --- a/modflow_devtools/dfn/parse.py +++ b/modflow_devtools/dfn/parser.py @@ -63,7 +63,7 @@ def try_parse_bool(value: Any) -> Any: } -def try_parse_parent(meta: list[str]) -> "str | list[str] | None": +def try_get_parent(meta: list[str]) -> "str | list[str] | None": """ Try to parse a component's parent from its metadata. @@ -104,7 +104,7 @@ def is_multi_package(meta: list[str]) -> bool: return any("multi-package" in m for m in meta) -def parse_mf6_subpackages(meta: list[str]) -> list[str]: +def get_subpackages(meta: list[str]) -> list[str]: """ Return MF6 subpackage abbreviations declared via '# mf6 subpackage '. diff --git a/modflow_devtools/dfn/v1.py b/modflow_devtools/dfn/schema.py similarity index 74% rename from modflow_devtools/dfn/v1.py rename to modflow_devtools/dfn/schema.py index e967c8ed..b3179eeb 100644 --- a/modflow_devtools/dfn/v1.py +++ b/modflow_devtools/dfn/schema.py @@ -5,8 +5,6 @@ a function to fetch DFNs from the MF6 repository. """ -import shutil -import tempfile from ast import literal_eval from collections.abc import Mapping from itertools import groupby @@ -15,7 +13,6 @@ from typing import ( Any, Literal, - Optional, TypedDict, ) from warnings import warn @@ -24,11 +21,7 @@ from boltons.dictutils import OMD from boltons.iterutils import remap -from modflow_devtools.download import download_and_unzip - -# TODO: use dataclasses instead of typed dicts? static -# methods on typed dicts are evidently not allowed -# mypy: ignore-errors +from modflow_devtools.dfn import parser def _try_literal_eval(value: str) -> Any: @@ -90,10 +83,24 @@ def _field_attr_sort_key(item) -> int: return 8 +def block_sort_key(item: tuple[str, Any]) -> int: + """Sort blocks in canonical MF6 order.""" + order = ["options", "dimensions", "griddata", "packagedata", "connectiondata", "period"] + name = item[0] + try: + return order.index(name) + except ValueError: + return len(order) + + FormatVersion = Literal[1, 2] """DFN format version number.""" +DfnFormat = Literal["dfn", "toml"] +"""DFN serialization format.""" + + FieldType = Literal[ "keyword", "integer", @@ -113,11 +120,8 @@ def _field_attr_sort_key(item) -> int: ] -_SCALAR_TYPES = FieldType.__args__[:4] - - -Dfns = dict[str, "Dfn"] -Fields = dict[str, "Field"] +_SCALAR_TYPES = ("keyword", "integer", "double precision", "string") +SCALAR_TYPES = _SCALAR_TYPES # public alias class Field(TypedDict): @@ -125,12 +129,34 @@ class Field(TypedDict): name: str type: FieldType - shape: Any | None = None block: str | None = None default: Any | None = None - children: Optional["Fields"] = None + longname: str | None = None description: str | None = None + optional: bool = False + developmode: bool = False + shape: str | None = None + valid: tuple[str, ...] | None = None + netcdf: bool = False + tagged: bool = False reader: Reader = "urword" + in_record: bool = False + layered: bool | None = None + preserve_case: bool = False + numeric_index: bool = False + deprecated: bool = False + removed: bool = False + mf6internal: str | None = None + block_variable: bool = False + just_data: bool = False + time_series: bool = False + + # for composite fields + children: Mapping[str, "Field"] = None + + +Fields = Mapping[str, "Field"] +Blocks = Mapping[str, Fields] class Ref(TypedDict): @@ -169,6 +195,9 @@ class Sln(TypedDict): pattern: str +Dfns = dict[str, "Dfn"] + + class Dfn(TypedDict): """ MODFLOW 6 input definition. An input definition @@ -210,12 +239,17 @@ class Dfn(TypedDict): Distinct from fkeys, which are field-level references. """ + schema_version: str name: str + ftype: str | None = None + parent: str | list[str] | None = None + blocks: Blocks | None = None + children: Dfns | None = None advanced: bool = False multi: bool = False ref: Ref | None = None sln: Sln | None = None - fkeys: Dfns | None = None + fkeys: Dfns | None = None # deprecated subcomponents: list[str] | None = None @staticmethod @@ -590,83 +624,188 @@ def load( cls, f, name: str | None = None, - version: FormatVersion = 1, + version: FormatVersion | DfnFormat = "dfn", **kwargs, ) -> "Dfn": """ Load a component definition from a definition file. """ - if version == 1: + if version in ["dfn", 1]: return cls._load_v1(f, name, **kwargs) - elif version == 2: + elif version in ["toml", 2]: return cls._load_v2(f, name) else: raise ValueError(f"Unsupported version, expected one of {version.__args__}") @staticmethod - def _load_all_v1(dfndir: PathLike) -> Dfns: - paths: list[Path] = [p for p in dfndir.glob("*.dfn") if p.stem not in ["common", "flopy"]] + def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: + """Load all component definitions from the given directory.""" - # load common variables - common_path: Path | None = dfndir / "common.dfn" - if not common_path.is_file(): - common = None - else: - with common_path.open() as f: - common, _ = Dfn._load_v1_flat(f) - - # load references (subpackages) - refs = {} - for path in paths: - with path.open() as f: - dfn = Dfn.load(f, name=path.stem, common=common) - ref = dfn.get("ref", None) - if ref: - refs[ref["key"]] = ref + if version: + warn("load_all() argument 'version' is deprecated and ignored") - # load definitions dfns: Dfns = {} - for path in paths: - with path.open() as f: - dfn = Dfn.load(f, name=path.stem, common=common, refs=refs) - dfns[path.stem] = dfn + + dfn_paths: list[Path] = [ + p for p in dfndir.glob("*.dfn") if p.stem not in ["common", "flopy"] + ] + toml_paths: list[Path] = [ + p for p in dfndir.glob("*.toml") if p.stem not in ["common", "flopy"] + ] + + if any(dfn_paths) and any(toml_paths): + raise ValueError("Directory contains both DFN and TOML definition files") + if not any(dfn_paths) and not any(toml_paths): + raise ValueError("Directory contains no definition files") + + if any(dfn_paths): + # load common fields + common_path: Path | None = dfndir / "common.dfn" + if not common_path.is_file(): + common = None + else: + with common_path.open() as f: + common, _ = Dfn._load_v1_flat(f) + + # load subpackages + refs = {} + for path in dfn_paths: + with path.open() as f: + dfn = Dfn.load(f, name=path.stem, common=common) + ref = dfn.get("ref", None) + if ref: + refs[ref["key"]] = ref + + # load definitions + for path in dfn_paths: + with path.open() as f: + dfn = Dfn.load(f, name=path.stem, common=common, refs=refs) + dfns[path.stem] = dfn + else: + for path in toml_paths: + with path.open(mode="rb") as f: + dfn = Dfn.load(f, name=path.stem) + dfns[path.stem] = dfn return dfns - @staticmethod - def _load_all_v2(dfndir: PathLike) -> Dfns: - paths: list[Path] = [p for p in dfndir.glob("*.toml") if p.stem not in ["common", "flopy"]] - dfns: Dfns = {} - for path in paths: - with path.open(mode="rb") as f: - dfn = Dfn.load(f, name=path.stem, version=2) - dfns[path.stem] = dfn - return dfns +def _load_common(f: Any) -> tuple[OMD, list[str]]: + common, _ = parser.parse_dfn(f) + return common - @staticmethod - def load_all(dfndir: PathLike, version: FormatVersion = 1) -> Dfns: - """Load all component definitions from the given directory.""" - if version == 1: - return Dfn._load_all_v1(dfndir) - elif version == 2: - return Dfn._load_all_v2(dfndir) - else: - raise ValueError(f"Unsupported version, expected one of {version.__args__}") +load_common = _load_common # public alias + + +def load(f: Any, format: str = "dfn", **kwargs: Any) -> Dfn: + """Load a v1 definition file.""" + + if format != "dfn": + raise ValueError(f"Unsupported format: {format!r}. Expected 'dfn'.") + + name = kwargs.pop("name") + fields, meta = parser.parse_dfn(f, **kwargs) + parent = parser.try_get_parent(meta) + blocks = { + block_name: {field["name"]: Field(field) for field in block} + for block_name, block in groupby(fields.values(multi=True), lambda fd: fd["block"]) + } + multi = parser.is_multi_package(meta) + advanced = parser.is_advanced_package(meta) + subcomponents = parser.get_subpackages(meta) or None + + return Dfn( + schema_version="1", + name=name, + parent=parent, + blocks=blocks, + multi=multi, + advanced=advanced, + subcomponents=subcomponents, + ) + + +EXCLUDE_DFNS = ["common.dfn", "flopy.dfn"] + + +def load_all(path: str | PathLike) -> Dfns: + """Load definition files in a directory.""" + path = Path(path).expanduser().resolve() + dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.name not in EXCLUDE_DFNS} + dfns: Dfns = {} + if dfn_paths: + with (path / "common.dfn").open() as f: + common = _load_common(f) + for dfn_name, dfn_path in dfn_paths.items(): + with dfn_path.open() as f: + dfns[dfn_name] = load(f, name=dfn_name, common=common, format="dfn") + return dfns + + +def get_fields(dfn: Dfn) -> OMD: + """Combined map of fields from all blocks (flat, top-level only).""" + items = [] + for block in (dfn["blocks"] or {}).values(): + for f in block.values(): + items.append((f["name"], f)) + return OMD(items) + + +def infer_parent(dfn: Dfn) -> str | None: + """Infer a component's parent using naming conventions.""" + if dfn["name"] == "sim-nam": + return None + if dfn["name"].endswith("-nam"): + return "sim-nam" + if dfn["name"].startswith(("exg-", "sln-", "utl-")): + return "sim-nam" + if "-" in dfn["name"]: + mdl = dfn["name"].split("-")[0] + return f"{mdl}-nam" + return None + + +def resolve_parent(dfn: Dfn) -> Dfn: + """Infer and set a component's parent using naming conventions.""" + parent = infer_parent(dfn) + dfn["parent"] = parent + return dfn + + +def resolve_parents(dfns: Dfns) -> Dfns: + """Infer and set component parents using naming conventions.""" + return {name: resolve_parent(dfn) for name, dfn in dfns.items()} + + +def to_tree(dfns: Dfns) -> Dfn: + """Condense flat definitions to a hierarchical definition.""" + + if (first_dfn := next(iter(dfns.values()), None))["schema_version"] != "1": + raise ValueError(f"Expected schema version 1, got {first_dfn['schema_version']!r}") + + dfns = resolve_parents(dfns) + roots = {name: dfn for name, dfn in dfns.items() if dfn["parent"] is None} + if (nroots := len(roots)) != 1: + raise ValueError(f"Expected one root component, found {nroots}") + + def _to_tree(dfn: Dfn) -> Dfn: + children = {name: _dfn for name, _dfn in dfns.items() if _dfn["parent"] == dfn["name"]} + dfn["children"] = {name: _to_tree(_dfn) for name, _dfn in children.items()} or None + return dfn -def get_dfns(owner: str, repo: str, ref: str, outdir: str | PathLike, verbose: bool = False): - """Fetch definition files from the MODFLOW 6 repository.""" - url = f"https://github.com/{owner}/{repo}/archive/{ref}.zip" - if verbose: - print(f"Downloading MODFLOW 6 repository from {url}") - with tempfile.TemporaryDirectory() as tmp: - dl_path = download_and_unzip(url, tmp, verbose=verbose) - contents = list(dl_path.glob("modflow6-*")) - proj_path = next(iter(contents), None) - if not proj_path: - raise ValueError(f"Missing proj dir in {dl_path}, found {contents}") - if verbose: - print("Copying dfns from download dir to output dir") - shutil.copytree(proj_path / "doc" / "mf6io" / "mf6ivar" / "dfn", outdir, dirs_exist_ok=True) + return _to_tree(next(iter(roots.values()))) + + +def to_flat(dfn: Dfn) -> Dfns: + """Flatten a hierarchical definition into its constituent definitions.""" + + def _to_flat(_dfn: Dfn) -> Dfns: + result: Dfns = {_dfn["name"]: _dfn} + result[_dfn["name"]]["children"] = None + for child in (_dfn["children"] or {}).values(): + result.update(_to_flat(child)) + return result + + return _to_flat(dfn) diff --git a/modflow_devtools/dfn/v1_1.py b/modflow_devtools/dfn/v1_1.py deleted file mode 100644 index b88424f6..00000000 --- a/modflow_devtools/dfn/v1_1.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any, Literal - -from boltons.dictutils import OMD -from packaging.version import Version - - -@dataclass(kw_only=True) -class FieldV1_1: - name: str - type: str | None = None - block: str | None = None - default: Any | None = None - longname: str | None = None - description: str | None = None - children: Mapping[str, FieldV1_1] | None = None - optional: bool = False - developmode: bool = False - shape: str | None = None - valid: tuple[str, ...] | None = None - netcdf: bool = False - tagged: bool = False - - @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> FieldV1_1: - keys = set(cls.__dataclass_fields__.keys()) - if strict: - if extra_keys := set(d.keys()) - keys: - raise ValueError(f"Unrecognized keys in field data: {extra_keys}") - return cls(**{k: v for k, v in d.items() if k in keys}) - - -Block = Mapping[str, FieldV1_1] -Blocks = Mapping[str, Block] - - -def block_sort_key(item: tuple[str, Any]) -> int: - """Sort blocks in canonical MF6 order.""" - order = ["options", "dimensions", "griddata", "packagedata", "connectiondata", "period"] - name = item[0] - try: - return order.index(name) - except ValueError: - return len(order) - - -FieldType = Literal[ - "keyword", - "integer", - "double precision", - "string", - "record", - "recarray", - "keystring", -] - -SCALAR_TYPES = ("keyword", "integer", "double precision", "string") - -Reader = Literal[ - "urword", - "u1ddbl", - "u2ddbl", - "readarray", -] - - -@dataclass(kw_only=True) -class FieldV1(FieldV1_1): - # V1-specific attributes - reader: Reader = "urword" - in_record: bool = False - layered: bool | None = None - preserve_case: bool = False - numeric_index: bool = False - deprecated: bool = False - removed: bool = False - mf6internal: str | None = None - block_variable: bool = False - just_data: bool = False - time_series: bool = False - - @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> FieldV1: - keys = set(cls.__dataclass_fields__.keys()) - if strict: - if extra_keys := set(d.keys()) - keys: - raise ValueError(f"Unrecognized keys in field data: {extra_keys}") - return cls(**{k: v for k, v in d.items() if k in keys}) - - -@dataclass(kw_only=True) -class Dfn: - """ - MODFLOW 6 input component definition (v1 / v1.1 schema). - - Attributes - ---------- - schema_version : Version - Schema version of this definition. - name : str - Component name (e.g., "gwf-chd", "sim-nam"). - parent : str | list[str] | None - Valid parent component type(s). - advanced : bool - Whether this is an advanced package. - multi : bool - Whether this is a multi-package. - ftype : str | None - MODFLOW 6 file type string, if applicable. - blocks : Blocks | None - Block definitions containing field specifications. - children : Dfns | None - Child component instances (populated by to_tree). - subcomponents : list[str] | None - Allowed child component types (schema-level constraint). - """ - - schema_version: Version - name: str - parent: str | list[str] | None = None - advanced: bool = False - multi: bool = False - ftype: str | None = None - blocks: Blocks | None = None - children: Dfns | None = None - subcomponents: list[str] | None = None - - def __post_init__(self) -> None: - self.schema_version = Version(str(self.schema_version)) - if self.blocks is not None: - self.blocks = dict(sorted(self.blocks.items(), key=block_sort_key)) - - @property - def fields(self) -> OMD: - """Combined map of fields from all blocks (flat, top-level only).""" - items = [] - for block in (self.blocks or {}).values(): - for f in block.values(): - items.append((f.name, f)) - return OMD(items) - - @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> Dfn: - """ - Create a Dfn from a dictionary. - - Parameters - ---------- - d : dict - Dictionary containing DFN data. - strict : bool, optional - If True, raise ValueError for unrecognized keys at any level. - """ - from modflow_devtools.dfns.schema.v2 import FieldBase - - known_keys = set(cls.__dataclass_fields__.keys()) - schema_version = Version(str(d.get("schema_version", "2"))) - is_v1 = schema_version == Version("1") - - if strict: - extra = set(d.keys()) - known_keys - if extra: - raise ValueError(f"Unrecognized keys in DFN data: {extra}") - - data = {k: v for k, v in d.items() if k in known_keys} - data.setdefault("schema_version", schema_version) - - if blocks_raw := data.get("blocks"): - parsed_blocks: dict[str, Any] = {} - for block_name, block_data in blocks_raw.items(): - if not isinstance(block_data, dict): - parsed_blocks[block_name] = block_data - continue - block_fields: dict[str, Any] = {} - for field_name, field_data in block_data.items(): - if isinstance(field_data, dict): - if is_v1: - block_fields[field_name] = FieldV1.from_dict(field_data, strict=strict) - else: - block_fields[field_name] = FieldBase.from_dict( - field_data, strict=strict - ) - else: - block_fields[field_name] = field_data - parsed_blocks[block_name] = block_fields - data["blocks"] = parsed_blocks - - return cls(**data) - - -Dfns = dict[str, Dfn] diff --git a/modflow_devtools/dfn2toml.py b/modflow_devtools/dfn2toml.py index 7e5b5cdc..3f10182a 100644 --- a/modflow_devtools/dfn2toml.py +++ b/modflow_devtools/dfn2toml.py @@ -1,118 +1,111 @@ -"""Convert MODFLOW 6 DFN files to TOML (v1, v1.1, or v2 schema).""" +"""Convert MODFLOW 6 DFN files to TOML.""" import argparse -import sys -import textwrap from os import PathLike from pathlib import Path +from typing import Any import tomli_w as tomli -from boltons.iterutils import remap - -from modflow_devtools.dfn.mapper import ( - _dfn_to_plain_dict, - _load_common, - _toml_safe, - is_valid, - load, - load_flat, - to_flat, - to_tree, -) -from modflow_devtools.misc import drop_none_or_empty +from pydantic import BaseModel + +from modflow_devtools.dfn import schema as v1 +from modflow_devtools.dfn.mapper import map as map_v1_1 +from modflow_devtools.dfns.mapper import map as map_v2 + + +def _toml_safe(obj: Any) -> Any: + """ + Recursively coerce non-TOML-native types to containers + and primitives suitable for TOML serialization. + """ + + if isinstance(obj, BaseModel): + return obj.model_dump( + exclude_none=True, + exclude_unset=True, + exclude_defaults=True, + ) + if isinstance(obj, dict): + return {k: _toml_safe(v) for k, v in obj.items() if v is not None} + if isinstance(obj, list): + return [_toml_safe(v) for v in obj] + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + return str(obj) # Version → str, etc. + # mypy: ignore-errors -def convert(inpath: PathLike, outdir: PathLike, schema: str = "1") -> None: - """Convert DFN file(s) to TOML. +def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str = "2") -> None: + """Migrate DFN files' schema version and convert to TOML. Parameters ---------- - inpath : PathLike + inpath : str or PathLike Input file or directory. - outdir : PathLike + outdir : str or PathLike Output directory. - schema : str - Target schema version: "1", "1.1", or "2". + schema_version : str, optional + Target schema version: "1", "1.1", or "2". Default "2". """ inpath = Path(inpath).expanduser().absolute() outdir = Path(outdir).expanduser().absolute() outdir.mkdir(exist_ok=True, parents=True) - if schema not in ("1", "1.1", "2"): - raise ValueError(f"Unsupported schema version: {schema!r}. Expected '1', '1.1', or '2'.") - if inpath.is_file(): if inpath.name == "common.dfn": raise ValueError("Cannot convert common.dfn as a standalone file") common = {} - if (common_path := inpath.parent / "common.dfn").exists(): + if (common_path := inpath.parent / "common.dfn").is_file(): with common_path.open() as f: - common = _load_common(f) + common = v1.load_common(f) with inpath.open() as f: - dfn = load(f, name=inpath.stem, common=common, format="dfn") + dfn = v1.Dfn.load(f, name=inpath.stem, common=common) + + if schema_version == "1": + pass # nothing to do + elif schema_version == "1.1": + dfn = map_v1_1(dfn) + elif schema_version == "2": + dfn = map_v2(dfn) + else: + raise ValueError( + f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" + ) - _convert(dfn, outdir / f"{inpath.stem}.toml", schema=schema) + dfn_path = outdir / f"{inpath.stem}.toml" + with Path.open(dfn_path, "wb") as f: + tomli.dump(_toml_safe(dfn), f) else: - if schema == "1": - # v1: iterate files directly (no tree building) - dfns = load_flat(inpath) - for dfn_name, dfn in dfns.items(): - _convert(dfn, outdir / f"{dfn_name}.toml", schema=schema) + dfns = v1.load_all(inpath) + + if schema_version == "1": + pass # nothing to do + elif schema_version == "1.1": + dfns = {name: map_v1_1(dfn, "1.1") for name, dfn in dfns.items()} + dfns = v1.to_flat(v1.to_tree(dfns)) + elif schema_version == "2": + dfns = {name: map_v2(dfn, "2") for name, dfn in dfns.items()} else: - # v1.1 / v2: map all, build tree, flatten, convert - if schema == "1.1": - from modflow_devtools.dfn.mapper import map as map_v1_1 - - dfns = {name: map_v1_1(dfn, "1.1") for name, dfn in load_flat(inpath).items()} - else: - from modflow_devtools.dfns.mapper import map as map_v2 - - dfns = {name: map_v2(dfn, "2") for name, dfn in load_flat(inpath).items()} - - if schema == "1.1": - tree = to_tree(dfns) - flat = to_flat(tree) - for dfn_name, dfn in flat.items(): - _convert(dfn, outdir / f"{dfn_name}.toml", schema=schema) - else: - for dfn_name, component in dfns.items(): - _convert(component, outdir / f"{dfn_name}.toml", schema=schema) - - -def _convert(dfn_or_component: object, outpath: Path, schema: str = "1") -> None: - with Path.open(outpath, "wb") as f: - if schema == "2": - # Component is a Pydantic model - d = dfn_or_component.model_dump(exclude_none=True) # type: ignore[union-attr] - tomli.dump(_toml_safe(remap(d, visit=drop_none_or_empty)), f) - else: - # Dfn dataclass (v1 or v1.1) - dfn_dict = _dfn_to_plain_dict(dfn_or_component) # type: ignore[arg-type] - if blocks := dfn_dict.pop("blocks", None): - for block_name, block_fields in blocks.items(): - dfn_dict.setdefault(block_name, {}) - for field_name, field_data in block_fields.items(): - dfn_dict[block_name][field_name] = field_data - tomli.dump(_toml_safe(remap(dfn_dict, visit=drop_none_or_empty)), f) + raise ValueError( + f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" + ) + + for dfn_name, dfn in dfns.items(): + dfn_path = outdir / f"{dfn_name}.toml" + with Path.open(dfn_path, "wb") as f: + tomli.dump(_toml_safe(dfn), f) + + +convert = migrate # backwards-compatible alias if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Convert MODFLOW 6 DFN files to TOML.", - epilog=textwrap.dedent( - """\ -Convert MODFLOW 6 definition files (.dfn format) to TOML files. - -Schema versions: - 1 — v1 TOML: all original DFN attributes preserved, no schema change. - 1.1 — v1.1 TOML: v1-specific attributes stripped; shared base fields only. - 2 — v2 TOML: fully typed v2 Component schema (Pydantic model serialization). -""" - ), + description="Migrate DFN files' schema version and convert to TOML.", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( @@ -127,22 +120,11 @@ def _convert(dfn_or_component: object, outpath: Path, schema: str = "1") -> None help="Output directory.", ) parser.add_argument( - "--schema", + "--schema-version", "-s", - default="1", + default="2", choices=["1", "1.1", "2"], - help="Target schema version (default: 1).", - ) - parser.add_argument( - "--validate", - "-v", - action="store_true", - help="Validate DFN files without converting them.", + help="Target schema version (default: 2).", ) args = parser.parse_args() - - if args.validate: - if not is_valid(args.indir): - sys.exit(1) - else: - convert(args.indir, args.outdir, schema=args.schema) + migrate(indir=args.indir, outdir=args.outdir, schema_version=args.schema_version) diff --git a/modflow_devtools/dfns/__init__.py b/modflow_devtools/dfns/__init__.py index 081185cf..c87cc0c2 100644 --- a/modflow_devtools/dfns/__init__.py +++ b/modflow_devtools/dfns/__init__.py @@ -1,21 +1,18 @@ -""" -MODFLOW 6 definition file tools. -""" +"""Definition file tools""" import warnings -from os import PathLike -from pathlib import Path -from modflow_devtools.dfn.v1_1 import FieldV1 -from modflow_devtools.dfns.schema.v2 import ( +from modflow_devtools.dfn import fetch_dfns +from modflow_devtools.dfns.registry import DfnRegistry, LocalDfnRegistry, RemoteDfnRegistry +from modflow_devtools.dfns.schema import ( Array, Block, Blocks, Component, - DfnSpec, + Dfns, Double, FieldBase, - FieldV2, + File, Integer, Keyword, List, @@ -23,9 +20,6 @@ String, Union, ) -from modflow_devtools.dfns.schema.v2 import ( - Path as PathField, -) # Experimental API warning warnings.warn( @@ -44,104 +38,17 @@ "Blocks", "Component", "DfnRegistry", - "DfnRegistryDiscoveryError", - "DfnRegistryError", - "DfnRegistryNotFoundError", - "DfnSpec", + "Dfns", "Double", "FieldBase", - "FieldV1", - "FieldV2", + "File", "Integer", "Keyword", "List", "LocalDfnRegistry", - "PathField", "Record", "RemoteDfnRegistry", "String", "Union", - "get_dfn", - "get_dfn_path", - "get_registry", - "get_sync_status", - "is_valid", - "list_components", - "sync_dfns", + "fetch_dfns", ] - - -def is_valid(path: "str | PathLike", format: str = "dfn", verbose: bool = False) -> bool: - """Validate DFN file(s).""" - from modflow_devtools.dfn.mapper import is_valid as _is_valid - - return _is_valid(path, format=format, verbose=verbose) - - -# ============================================================================= -# Registry lazy-import machinery -# ============================================================================= - - -def _get_registry_module(): - from modflow_devtools.dfns import registry - - return registry - - -def __getattr__(name: str): - registry_exports = { - "DfnRegistry", - "DfnRegistryDiscoveryError", - "DfnRegistryError", - "DfnRegistryNotFoundError", - "LocalDfnRegistry", - "RemoteDfnRegistry", - "get_registry", - "get_sync_status", - "sync_dfns", - } - if name in registry_exports: - registry = _get_registry_module() - return getattr(registry, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -# ============================================================================= -# Module-level convenience functions -# ============================================================================= - - -def get_dfn( - component: str, - ref: str = "develop", - source: str = "modflow6", - path: "str | PathLike | None" = None, -) -> "Component": - """Get a component definition by name from the registry.""" - registry = _get_registry_module() - reg = registry.get_registry(source=source, ref=ref, path=path) - return reg.get_dfn(component) - - -def get_dfn_path( - component: str, - ref: str = "develop", - source: str = "modflow6", - path: "str | PathLike | None" = None, -) -> Path: - """Get the local cached file path for a DFN component.""" - registry = _get_registry_module() - reg = registry.get_registry(source=source, ref=ref, path=path) - return reg.get_dfn_path(component) - - -def list_components( - ref: str = "develop", - source: str = "modflow6", - path: "str | PathLike | None" = None, -) -> list[str]: - """List available components for a registry.""" - registry = _get_registry_module() - reg = registry.get_registry(source=source, ref=ref, path=path) - return list(reg.spec.components.keys()) diff --git a/modflow_devtools/dfns/__main__.py b/modflow_devtools/dfns/__main__.py index 5c18ad41..6862cabb 100644 --- a/modflow_devtools/dfns/__main__.py +++ b/modflow_devtools/dfns/__main__.py @@ -2,10 +2,9 @@ Command-line interface for the DFNs API. Usage: - mf dfns sync [--ref REF] [--force] - mf dfns info - mf dfns list [--ref REF] - mf dfns clean [--all] + python -m modflow_devtools.dfns sync + python -m modflow_devtools.dfns info + python -m modflow_devtools.dfns clean """ from __future__ import annotations @@ -14,236 +13,78 @@ import shutil import sys -from modflow_devtools.dfns.registry import ( - DfnRegistryDiscoveryError, - DfnRegistryNotFoundError, - get_bootstrap_config, - get_cache_dir, - get_registry, - get_sync_status, - sync_dfns, -) +from modflow_devtools.dfns.registry import RemoteDfnRegistry, is_cached def cmd_sync(args: argparse.Namespace) -> int: - """Sync DFN registries from remote sources.""" - source = args.source - ref = args.ref - force = args.force - - try: - if ref: - print(f"Syncing {source}@{ref}...") - registries = sync_dfns(source=source, ref=ref, force=force) - else: - print(f"Syncing all configured refs for {source}...") - registries = sync_dfns(source=source, force=force) - - for registry in registries: - meta = registry.registry_meta - print(f" {registry.ref}: {len(meta.files)} files") - - print(f"Synced {len(registries)} registry(ies)") - return 0 - - except DfnRegistryNotFoundError as e: - print(f"Error: {e}", file=sys.stderr) - return 1 - except DfnRegistryDiscoveryError as e: - print(f"Error: {e}", file=sys.stderr) - return 1 - except Exception as e: - print(f"Unexpected error: {e}", file=sys.stderr) - return 1 + """Sync DFN releases from GitHub release assets.""" - -def cmd_info(args: argparse.Namespace) -> int: - """Show sync status and cache information.""" - source = args.source + registries = RemoteDfnRegistry.load_default() try: - config = get_bootstrap_config() - - if source not in config.sources: - print(f"Unknown source: {source}", file=sys.stderr) - print(f"Available sources: {list(config.sources.keys())}", file=sys.stderr) - return 1 - - source_config = config.sources[source] - print(f"Source: {source}") - print(f" Repository: {source_config.repo}") - print(f" DFN path: {source_config.dfn_path}") - print(f" Registry path: {source_config.registry_path}") - print() - - # Show sync status - status = get_sync_status(source=source) - print("Configured refs:") - for ref, synced in status.items(): - status_str = "synced" if synced else "not synced" - print(f" {ref}: {status_str}") - print() - - # Show cache info - cache_dir = get_cache_dir("dfn") - if cache_dir.exists(): - # Count cached files - registries_dir = cache_dir / "registries" / source - files_dir = cache_dir / "files" / source - - registry_count = 0 - file_count = 0 - total_size = 0 - - if registries_dir.exists(): - for p in registries_dir.rglob("*"): - if p.is_file(): - registry_count += 1 - total_size += p.stat().st_size - - if files_dir.exists(): - for p in files_dir.rglob("*"): - if p.is_file(): - file_count += 1 - total_size += p.stat().st_size - - print(f"Cache directory: {cache_dir}") - print(f" Registries: {registry_count}") - print(f" DFN files: {file_count}") - print(f" Total size: {_format_size(total_size)}") - else: - print("Cache directory: (not created)") - - return 0 - + for registry in registries.values(): + print(f"Syncing {registry.release_id}...") + registry.sync(force=args.force) + n_files = ( + len(list(registry.cache_path.glob("*.*"))) if registry.cache_path.exists() else 0 + ) + print(f" {registry.release_id}: {n_files} files") + print(f"Synced {registry.release_id}") + return 0 except Exception as e: print(f"Error: {e}", file=sys.stderr) return 1 -def cmd_list(args: argparse.Namespace) -> int: - """List available components.""" - source = args.source - ref = args.ref +def cmd_info(args: argparse.Namespace) -> int: + """Show DFN release synchronization status.""" + registries = RemoteDfnRegistry.load_default() try: - registry = get_registry(source=source, ref=ref, auto_sync=True) - components = list(registry.spec.components.keys()) - - print(f"Components in {source}@{ref} ({len(components)} total):") - for component in sorted(components): - print(f" {component}") - + for registry in registries.values(): + if is_cached(registry.release_id): + print(f"Cached: {registry.release_id}") + else: + print(f"Uncached: {registry.release_id}") return 0 - - except DfnRegistryNotFoundError as e: - print(f"Error: {e}", file=sys.stderr) - print("Try running 'mf dfns sync' first.", file=sys.stderr) - return 1 except Exception as e: print(f"Error: {e}", file=sys.stderr) return 1 def cmd_clean(args: argparse.Namespace) -> int: - """Clean the cache directory.""" - source = args.source - clean_all = args.all - - cache_dir = get_cache_dir("dfn") - - if not cache_dir.exists(): - print("Cache directory does not exist.") - return 0 - - if clean_all: - # Clean entire cache - print(f"Removing entire cache directory: {cache_dir}") - shutil.rmtree(cache_dir) - print("Cache cleaned.") - else: - # Clean only the specified source - registries_dir = cache_dir / "registries" / source - files_dir = cache_dir / "files" / source + """Clean the DFN release cache directory.""" - removed = False - if registries_dir.exists(): - print(f"Removing registries for {source}: {registries_dir}") - shutil.rmtree(registries_dir) - removed = True - - if files_dir.exists(): - print(f"Removing files for {source}: {files_dir}") - shutil.rmtree(files_dir) - removed = True - - if removed: - print(f"Cache cleaned for {source}.") - else: - print(f"No cache found for {source}.") + cache_dir = RemoteDfnRegistry.base_cache_path() + print(f"Cleaning cache directory: {cache_dir}") + shutil.rmtree(cache_dir, ignore_errors=True) + print("Cache cleaned.") return 0 -def _format_size(size_bytes: int) -> str: - """Format size in bytes to human-readable string.""" - size = float(size_bytes) - for unit in ["B", "KB", "MB", "GB"]: - if size < 1024: - return f"{size:.1f} {unit}" - size /= 1024 - return f"{size:.1f} TB" - - def main(argv: list[str] | None = None) -> int: """Main entry point for the CLI.""" parser = argparse.ArgumentParser( - prog="mf dfns", + prog="python -m modflow_devtools.dfns", description="MODFLOW 6 definition file tools", ) - parser.add_argument( - "--source", - "-s", - default="modflow6", - help="Source repository name (default: modflow6)", - ) - subparsers = parser.add_subparsers(dest="command", help="Available commands") - # sync command - sync_parser = subparsers.add_parser("sync", help="Sync DFN registries from remote") - sync_parser.add_argument( - "--ref", - "-r", - help="Specific ref to sync (default: all configured refs)", - ) + # sync + sync_parser = subparsers.add_parser("sync", help="Sync DFN files from release assets") sync_parser.add_argument( "--force", "-f", action="store_true", - help="Force re-sync even if already cached", + help="Force re-download even if already cached", ) - # info command - subparsers.add_parser("info", help="Show sync status and cache info") - - # list command - list_parser = subparsers.add_parser("list", help="List available components") - list_parser.add_argument( - "--ref", - "-r", - default="develop", - help="Git ref to list components from (default: develop)", - ) + # info + subparsers.add_parser("info", help="Show cache info and sync status") - # clean command - clean_parser = subparsers.add_parser("clean", help="Clean the cache") - clean_parser.add_argument( - "--all", - "-a", - action="store_true", - help="Clean entire cache, not just the specified source", - ) + # clean + subparsers.add_parser("clean", help="Clean the cache") args = parser.parse_args(argv) @@ -251,12 +92,10 @@ def main(argv: list[str] | None = None) -> int: parser.print_help() return 0 - if args.command == "sync": + elif args.command == "sync": return cmd_sync(args) elif args.command == "info": return cmd_info(args) - elif args.command == "list": - return cmd_list(args) elif args.command == "clean": return cmd_clean(args) else: diff --git a/modflow_devtools/dfns/dfns.toml b/modflow_devtools/dfns/dfns.toml index 4a84ae67..5680809c 100644 --- a/modflow_devtools/dfns/dfns.toml +++ b/modflow_devtools/dfns/dfns.toml @@ -1,24 +1,8 @@ -# DFNs API bootstrap configuration -# -# This file tells modflow-devtools where to find DFN registries. -# Users can override or extend this by creating a config file at: +# This file tells modflow-devtools where to find DFN releases. +# Users can override or extend this with a config file at: # - Linux/macOS: ~/.config/modflow-devtools/dfns.toml # - Windows: %APPDATA%/modflow-devtools/dfns.toml -[sources.modflow6] -# GitHub repository containing DFN files -repo = "MODFLOW-ORG/modflow6" - -# Path within the repository to the DFN files directory -dfn_path = "doc/mf6io/mf6ivar/dfn" - -# Path within the repository to the registry metadata file -registry_path = ".registry/dfns.toml" - -# Git refs (branches, tags, commit hashes) to sync by default -refs = [ - "develop", - "6.6.0", - "6.5.0", - "6.4.4", +releases = [ + "MODFLOW-ORG/modflow6-nightly-build@20260518" ] diff --git a/modflow_devtools/dfns/fetch.py b/modflow_devtools/dfns/fetch.py deleted file mode 100644 index ecbb7b28..00000000 --- a/modflow_devtools/dfns/fetch.py +++ /dev/null @@ -1,25 +0,0 @@ -from os import PathLike -from pathlib import Path -from shutil import copytree -from tempfile import TemporaryDirectory - -from modflow_devtools.download import download_and_unzip - - -def fetch_dfns(owner: str, repo: str, ref: str, outdir: str | PathLike, verbose: bool = False): - """Fetch definition files from the MODFLOW 6 repository.""" - url = f"https://github.com/{owner}/{repo}/archive/{ref}.zip" - if verbose: - print(f"Downloading MODFLOW 6 repository archive from {url}") - with TemporaryDirectory() as tmp: - dl_path = download_and_unzip(url, Path(tmp), verbose=verbose) - contents = list(dl_path.glob("modflow6-*")) - proj_path = next(iter(contents), None) - if not proj_path: - raise ValueError(f"Missing proj dir in {dl_path}, found {contents}") - if verbose: - print("Copying dfns from download dir to output dir") - copytree(proj_path / "doc" / "mf6io" / "mf6ivar" / "dfn", outdir, dirs_exist_ok=True) - - -get_dfns = fetch_dfns # alias for backward compatibility diff --git a/modflow_devtools/dfns/make_registry.py b/modflow_devtools/dfns/make_registry.py deleted file mode 100644 index bd510aa0..00000000 --- a/modflow_devtools/dfns/make_registry.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Registry generation tool for DFN files. - -This tool scans a directory of DFN files and generates a registry file -that can be used by the DFNs API for discovery and verification. - -Usage: - python -m modflow_devtools.dfn.make_registry --dfn-path PATH --output FILE [--ref REF] - -Example (for MODFLOW 6 CI): - python -m modflow_devtools.dfn.make_registry \\ - --dfn-path doc/mf6io/mf6ivar/dfn \\ - --output .registry/dfns.toml \\ - --ref ${{ github.ref_name }} -""" - -from __future__ import annotations - -import argparse -import hashlib -import sys -from datetime import datetime, timezone -from pathlib import Path - -import tomli_w - - -def compute_file_hash(path: Path) -> str: - """Compute SHA256 hash of a file.""" - sha256 = hashlib.sha256() - with path.open("rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - sha256.update(chunk) - return f"sha256:{sha256.hexdigest()}" - - -def scan_dfn_directory(dfn_path: Path) -> dict[str, str]: - """ - Scan a directory for DFN files and compute their hashes. - - Parameters - ---------- - dfn_path : Path - Path to directory containing DFN files. - - Returns - ------- - dict[str, str] - Map of filename to SHA256 hash. - """ - files = {} - - # Find all .dfn files - for p in sorted(dfn_path.glob("*.dfn")): - files[p.name] = compute_file_hash(p) - - # Find all .toml files (spec.toml and/or component files) - for p in sorted(dfn_path.glob("*.toml")): - files[p.name] = compute_file_hash(p) - - return files - - -def generate_registry( - dfn_path: Path, - output_path: Path, - ref: str | None = None, - devtools_version: str | None = None, -) -> None: - """ - Generate a DFN registry file. - - Parameters - ---------- - dfn_path : Path - Path to directory containing DFN files. - output_path : Path - Path to write the registry file. - ref : str, optional - Git ref this registry is being generated for. - devtools_version : str, optional - Version of modflow-devtools generating this registry. - """ - # Scan directory for files - files = scan_dfn_directory(dfn_path) - - if not files: - raise ValueError(f"No DFN files found in {dfn_path}") - - # Get devtools version if not provided - if devtools_version is None: - try: - from modflow_devtools import __version__ - - devtools_version = __version__ - except ImportError: - devtools_version = "unknown" - - # Build registry structure - registry: dict = { - "schema_version": "1.0", - "generated_at": datetime.now(timezone.utc).isoformat(), - "devtools_version": devtools_version, - } - - if ref: - registry["metadata"] = {"ref": ref} - - # Add files section - registry["files"] = {filename: {"hash": file_hash} for filename, file_hash in files.items()} - - # Write registry file - output_path.parent.mkdir(parents=True, exist_ok=True) - with output_path.open("wb") as f: - tomli_w.dump(registry, f) - - -def main(argv: list[str] | None = None) -> int: - """Main entry point.""" - parser = argparse.ArgumentParser( - prog="python -m modflow_devtools.dfn.make_registry", - description="Generate a DFN registry file", - ) - parser.add_argument( - "--dfn-path", - "-d", - type=Path, - required=True, - help="Path to directory containing DFN files", - ) - parser.add_argument( - "--output", - "-o", - type=Path, - required=True, - help="Output path for registry file", - ) - parser.add_argument( - "--ref", - "-r", - help="Git ref this registry is being generated for", - ) - parser.add_argument( - "--devtools-version", - help="Version of modflow-devtools (default: auto-detect)", - ) - - args = parser.parse_args(argv) - - dfn_path = args.dfn_path.expanduser().resolve() - output_path = args.output.expanduser().resolve() - - if not dfn_path.exists(): - print(f"Error: DFN path does not exist: {dfn_path}", file=sys.stderr) - return 1 - - if not dfn_path.is_dir(): - print(f"Error: DFN path is not a directory: {dfn_path}", file=sys.stderr) - return 1 - - try: - generate_registry( - dfn_path=dfn_path, - output_path=output_path, - ref=args.ref, - devtools_version=args.devtools_version, - ) - - # Report results - files = scan_dfn_directory(dfn_path) - print(f"Generated registry: {output_path}") - print(f" Files: {len(files)}") - if args.ref: - print(f" Ref: {args.ref}") - - return 0 - - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index 5192afa1..41ecac5c 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -1,102 +1,187 @@ -""" -v2 schema mapping for MODFLOW 6 DFNs. -""" - from __future__ import annotations -import dataclasses import re -from dataclasses import asdict -from typing import Any, Literal, cast - -from boltons.dictutils import OMD -from packaging.version import Version +from typing import Any, Literal -from modflow_devtools.dfn.parse import ( - try_parse_bool, -) -from modflow_devtools.dfn.v1_1 import SCALAR_TYPES as V1_SCALAR_TYPES -from modflow_devtools.dfn.v1_1 import Dfn, FieldV1 -from modflow_devtools.dfns.schema.v2 import ( - Array, - Double, - FieldBase, - Integer, - Keyword, - List, - Record, - String, - Union, -) +from modflow_devtools.dfn import schema as v1 +from modflow_devtools.dfn.parser import try_parse_bool +from modflow_devtools.dfns import schema as v2 from modflow_devtools.misc import try_literal_eval _IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") +_LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") +_DIMS: frozenset[str] = frozenset( + {"nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert"} +) + + +def _resolve_dimensions(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + dims_referenced: set[str] = set() # dims used in shape expressions + dims_from_array: set[str] = set() # self-sizing array dimensions + dims_explicit: set[str] = set(_DIMS) # integer dimension fields + + # shape expressions may reference dimensions from several providers: + # - explicitly defined integer dimension fields + # - dynamic dimensions: size of 1D arrays + + def _scan_fields(fields: dict[str, v2.Field]) -> None: + for field in fields.values(): + if isinstance(field, v2.Array): + for dim in field.shape: + if _IDENT_RE.fullmatch(dim): + dims_referenced.add(dim) + else: + # no shape expr, self-sizing + dims_from_array.add(field.name) + scope = getattr(field, "dimension", None) + if scope in ("component", "model", "simulation"): + dims_explicit.add(field.name) + if isinstance(field, v2.Record): + _scan_fields(field.fields) + elif isinstance(field, v2.Union): + _scan_fields(field.arms) + elif isinstance(field, v2.List): + item = field.item + _scan_fields(item.fields if isinstance(item, v2.Record) else item.arms) + + for block in blocks.values(): + _scan_fields(block.fields) + + if not dims_referenced: + return blocks + + dims_provided: set[str] = dims_from_array & dims_explicit + + def _get_dims(record: v2.Record) -> set[str]: + found: set[str] = set() + for field in record.fields.values(): + if isinstance(field, v2.Array): + for dim in field.shape: + if _IDENT_RE.fullmatch(dim) and dim not in dims_provided: + # the shape expression of an array inside a record + # may reference a sibling integer subfield even if + # the integer is not marked as a dimension. + if (sibling := record.fields.get(dim, None)) is not None and isinstance( + sibling, v2.Integer + ): + found.add(dim) + return found + + def _resolve_fields(fields: dict[str, v2.Field]) -> dict[str, v2.Field]: + result = {} + for name, field in fields.items(): + if isinstance(field, v2.Array) and name in dims_provided: + field.dimension = "component" + elif isinstance(field, v2.Record): + local_dims = _get_dims(field) + subfields = _resolve_fields(field.fields) + if local_dims: + for subfield_name, subfield in subfields.items(): + if subfield_name in local_dims and isinstance(subfield, v2.Integer): + subfield.dimension = "record" + field.fields = subfields + elif isinstance(field, v2.Union): + field.arms = _resolve_fields(field.arms) + elif isinstance(field, v2.List): + if isinstance(field.item, v2.Record): + local_dims = _get_dims(field.item) + subfields = _resolve_fields(field.item.fields) + if local_dims: + for subfield_name, subfield in subfields.items(): + if subfield_name in local_dims and isinstance(subfield, v2.Integer): + subfield.dimension = "record" + field.item.fields = subfields + else: + field.item.arms = _resolve_fields(field.item.arms) + result[name] = field + return result + + def _resolve_block(block: v2.Block) -> v2.Block: + block.fields = _resolve_fields(block.fields) + return block + + return {block_name: _resolve_block(block) for block_name, block in blocks.items()} + + +def _resolve_relations(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: + pk_set: set[tuple[str, str]] = set() + fk_map: dict[tuple[str, str], str] = {} + + def _scan_fields(block_name: str, fields: dict[str, v2.Field]) -> None: + + def _scan_record(record: v2.Record) -> None: + for field in record.fields.values(): + if isinstance(field, v2.Array): + for dim in field.shape: + if m := _LOOKUP_RE.fullmatch(dim): + pk_block, _, fk_fname = m.groups() + sibling = record.fields.get(fk_fname) + if sibling is not None and getattr(sibling, "fk", None) is None: + fk_map[(block_name, fk_fname)] = f"{pk_block}.{fk_fname}" + pk_set.add((pk_block, fk_fname)) + + for field in fields.values(): + if isinstance(field, v2.Record): + _scan_record(field) + elif isinstance(field, v2.Union): + _scan_fields(block_name, field.arms) + elif isinstance(field, v2.List): + item = field.item + if isinstance(item, v2.Record): + _scan_record(item) + elif isinstance(item, v2.Union): + _scan_fields(block_name, item.arms) + + for block_name, block in blocks.items(): + _scan_fields(block_name, block.fields) + if not fk_map and not pk_set: + return blocks -# ============================================================================= -# Mapper -# ============================================================================= - - -class MapV1To2: - """Map a v1 Dfn (FieldV1 blocks) to a v2 Component.""" - - @staticmethod - def map_period_block(dfn: Dfn, block: dict) -> dict: - """Convert a period block recarray to individual arrays, one per column.""" - block = dict(block) - fields_list = list(block.values()) - - if fields_list and isinstance(fields_list[0], List): - assert len(fields_list) == 1 - list_field: List = fields_list[0] - block.pop(list_field.name) - item = list_field.item - columns: dict = dict(item.fields if isinstance(item, Record) else item.arms) - else: - columns = dict(block) - - cellid = columns.pop("cellid", None) - - _SCALAR_DTYPES = {"keyword", "integer", "double", "double precision", "string"} - - for col_name, column in columns.items(): - if isinstance(column, Array): - dtype = column.dtype - elif getattr(column, "type", None) in _SCALAR_DTYPES: - dtype = column.type - if dtype == "double precision": - dtype = "double" - else: - block[col_name] = column - continue - - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - old_dims = list(column.shape) if isinstance(column, Array) else [] - new_dims = ["nper"] - if cellid: - new_dims.append("nodes") - new_dims.extend(d for d in old_dims if d in GRID_DIM_NAMESPACE) - - block[col_name] = Array( - name=column.name, - longname=getattr(column, "longname", None), - description=getattr(column, "description", None), - optional=column.optional, - default=getattr(column, "default", None), - developmode=column.developmode, - netcdf=getattr(column, "netcdf", False), - dtype=dtype, - shape=new_dims, + def _resolve_fields(block_name: str, fields: dict[str, v2.Field]) -> dict[str, v2.Field]: + + def _resolve_record(record: v2.Record) -> v2.Record: + updates: dict = {} + for fname, sf in record.fields.items(): + updated = sf + if (block_name, fname) in fk_map and getattr(sf, "fk", None) is None: + updated = updated.model_copy(update={"fk": fk_map[(block_name, fname)]}) + if (block_name, fname) in pk_set and not getattr(sf, "pk", False): + updated = updated.model_copy(update={"pk": True}) + if updated is not sf: + updates[fname] = updated + if not updates: + return record + return record.model_copy( + update={"fields": {fn: updates.get(fn, sf) for fn, sf in record.fields.items()}} ) - return block + result = {} + for name, f in fields.items(): + if isinstance(f, v2.Record): + f = _resolve_record(f) + elif isinstance(f, v2.Union): + f.arms = _resolve_fields(block_name, f.arms) + elif isinstance(f, v2.List): + if isinstance(f.item, v2.Record): + f.item = _resolve_record(f.item) + else: + f.item.arms = _resolve_fields(block_name, f.item.arms) + result[name] = f + return result - @staticmethod - def map_field(dfn: Dfn, v1_field: FieldV1) -> FieldBase: - """Convert a v1 field to the appropriate v2 concrete type.""" - fields = cast(OMD, dfn.fields) + return {block_name: _resolve_fields(block, block_name) for block_name, block in blocks.items()} + + +def map(dfn: v1.Dfn) -> v2.Component: + """Map a component definition from the v1 schema to v2.""" + + if dfn["schema_version"] != "1": + raise ValueError(f"Expected schema version 1, got {dfn['schema_version']!r}") + + fields = v1.get_fields(dfn) + + def _map_field(field: v1.Field) -> v2.Field: def _to_bool(v: Any, default: bool = False) -> bool: if isinstance(v, bool): @@ -109,8 +194,8 @@ def _to_bool(v: Any, default: bool = False) -> bool: return False return default - def _map_field(f: FieldV1) -> FieldBase: - fd = asdict(f) + def __map_field(f: v1.Field) -> v2.Field: + fd = dict(f) fd = {k: try_parse_bool(v) for k, v in fd.items()} _name: str = fd["name"] @@ -150,9 +235,11 @@ def _parse_shape(s: str) -> list[str]: col_name = m.group(1) block_name = next( ( - fi.block + fi["block"] for fi in fields.values(multi=True) - if fi.name == col_name and fi.type == "integer" and fi.in_record + if fi["name"] == col_name + and fi["type"] == "integer" + and fi["in_record"] ), None, ) @@ -161,20 +248,20 @@ def _parse_shape(s: str) -> list[str]: else: provider = next( ( - fi.name + fi["name"] for fi in fields.values(multi=True) - if fi.type == "string" - and (fi.shape or "").strip() in (f"({elem})", elem) + if fi["type"] == "string" + and (fi["shape"] or "").strip() in (f"({elem})", elem) ), None, ) result.append(provider if provider else elem) return result - def _to_scalar() -> FieldBase: + def _to_scalar() -> v2.Scalar: assert _type is not None if _type == "keyword": - return Keyword( + return v2.Keyword( name=_name, longname=longname, description=description, @@ -184,7 +271,7 @@ def _to_scalar() -> FieldBase: netcdf=netcdf, ) if _type == "string": - return String( + return v2.String( name=_name, longname=longname, description=description, @@ -198,21 +285,19 @@ def _to_scalar() -> FieldBase: time_series=time_series, ) if _type == "integer": - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - v = [int(x) for x in valid] if valid else None if fd.get("block") == "dimensions": - if _name in GRID_DIM_NAMESPACE: + if _name in _DIMS: _dim_scope: ( Literal["record", "component", "model", "simulation"] | None ) = "model" - elif dfn.name == "sim-tdis" and _name == "nper": + elif dfn["name"] == "sim-tdis" and _name == "nper": _dim_scope = "simulation" else: _dim_scope = "component" else: _dim_scope = None - return Integer( + return v2.Integer( name=_name, longname=longname, description=description, @@ -226,7 +311,7 @@ def _to_scalar() -> FieldBase: dimension=_dim_scope, ) if _type in ("double", "double precision"): - return Double( + return v2.Double( name=_name, longname=longname, description=description, @@ -239,15 +324,15 @@ def _to_scalar() -> FieldBase: ) raise TypeError(f"Unsupported scalar type: {_type!r}") - def _row_field() -> Record | Union: + def _row_field() -> v2.Record | v2.Union: item_names = (_type or "").split()[1:] if not item_names: raise ValueError(f"Missing list item definition: {_type!r}") item_types = [ - fi.type + fi["type"] for fi in fields.values(multi=True) - if fi.name in item_names and fi.in_record + if fi["name"] in item_names and fi["in_record"] ] if ( @@ -258,16 +343,16 @@ def _row_field() -> Record | Union: or (item_types[0] or "").startswith("keystring") ) ): - mapped = MapV1To2.map_field(dfn, next(iter(fields.getlist(item_names[0])))) - if isinstance(mapped, (Record, Union)): + mapped = __map_field(next(iter(fields.getlist(item_names[0])))) + if isinstance(mapped, (v2.Record, v2.Union)): return mapped raise TypeError( f"Expected Record or Union for list item, got {type(mapped).__name__}" ) - if all(t in V1_SCALAR_TYPES for t in item_types): + if all(t in v1.SCALAR_TYPES for t in item_types): rec_fields = _record_fields() - return Record( + return v2.Record( name=_name, description=( (description or "").replace("is the list of", "is the record of") @@ -277,14 +362,14 @@ def _row_field() -> Record | Union: ) children = { - fi.name: MapV1To2.map_field(dfn, fi) + fi["name"]: __map_field(fi) for fi in fields.values(multi=True) - if fi.name in item_names and fi.in_record + if fi["name"] in item_names and fi["in_record"] } first = next(iter(children.values())) - if len(children) == 1 and isinstance(first, Union): + if len(children) == 1 and isinstance(first, v2.Union): return first - return Record( + return v2.Record( name=_name, description=( (description or "").replace("is the list of", "is the record of") or None @@ -295,9 +380,9 @@ def _row_field() -> Record | Union: def _union_fields() -> dict: names = (_type or "").split()[1:] return { - fi.name: MapV1To2.map_field(dfn, fi) + fi["name"]: __map_field(fi) for fi in fields.values(multi=True) - if fi.name in names and fi.in_record + if fi["name"] in names and fi["in_record"] } def _record_fields() -> dict: @@ -307,12 +392,12 @@ def _record_fields() -> dict: matches = [ fi for fi in fields.values(multi=True) - if fi.name == rname - and fi.in_record - and not (fi.type or "").startswith("record") + if fi["name"] == rname + and fi["in_record"] + and not (fi["type"] or "").startswith("record") ] if matches: - result[rname] = _map_field(matches[0]) + result[rname] = __map_field(matches[0]) return result if _type is None: @@ -320,7 +405,7 @@ def _record_fields() -> dict: if _type.startswith("recarray"): item = _row_field() - return List( + return v2.List( name=_name, longname=longname, description=description, @@ -333,7 +418,7 @@ def _record_fields() -> dict: if _type.startswith("keystring"): arms = _union_fields() - return Union( + return v2.Union( name=_name, longname=longname, description=description, @@ -345,7 +430,7 @@ def _record_fields() -> dict: if _type.startswith("record"): rec_fields = _record_fields() - return Record( + return v2.Record( name=_name, longname=longname, description=description, @@ -366,7 +451,20 @@ def _record_fields() -> dict: dtype = dtype_map.get(_type) if dtype is not None: if dtype == "string": - return Array( + # If the v1 shape is a single count identifier that isn't + # an explicit integer field (e.g. naux for auxiliary), the + # array defines that dimension by its length. + _str_dim: Literal["component", "model", "simulation"] | None = None + _parsed_str = _parse_shape(shape_str) + if len(_parsed_str) == 1: + _count_name = _parsed_str[0] + _is_explicit_int = any( + fi["name"] == _count_name and fi["type"] == "integer" + for fi in fields.values(multi=True) + ) + if not _is_explicit_int: + _str_dim = "component" + return v2.Array( name=_name, longname=longname, description=description, @@ -377,9 +475,10 @@ def _record_fields() -> dict: time_series=time_series, dtype="string", shape=[], + dimension=_str_dim, ) parsed_shape = _parse_shape(shape_str) - return Array( + return v2.Array( name=_name, longname=longname, description=description, @@ -394,290 +493,46 @@ def _record_fields() -> dict: return _to_scalar() - return _map_field(v1_field) - - @staticmethod - def _mark_dimension_fields(blocks: dict[str, dict]) -> dict[str, dict]: - """ - Post-pass: annotate every field that provides a dimension count. - - String-array dim providers (e.g. ``auxiliary``): marked - ``dimension="component"``. Record-local dim integers: marked - ``dimension="record"``. - """ - from modflow_devtools.dfns.schema.v2 import GRID_DIM_NAMESPACE - - shape_refs: set[str] = set() - string_array_names: set[str] = set() - explicit_globals: set[str] = set(GRID_DIM_NAMESPACE) - - def _scan(fields: dict) -> None: - for f in fields.values(): - if isinstance(f, Array): - if f.dtype != "string": - for elem in f.shape: - if _IDENT_RE.fullmatch(elem): - shape_refs.add(elem) - else: - string_array_names.add(f.name) - scope = getattr(f, "dimension", None) - if scope in ("component", "model", "simulation"): - explicit_globals.add(f.name) - if isinstance(f, Record): - _scan(f.fields) - elif isinstance(f, Union): - _scan(f.arms) - elif isinstance(f, List): - item = f.item - _scan(item.fields if isinstance(item, Record) else item.arms) - - for block_fields in blocks.values(): - _scan(block_fields) - - if not shape_refs: - return blocks - - string_provider_names: set[str] = string_array_names & shape_refs - global_dims: set[str] = explicit_globals | string_provider_names - - def _record_local_dims(rec: Record) -> set[str]: - to_mark: set[str] = set() - for sf in rec.fields.values(): - if isinstance(sf, Array) and sf.dtype != "string": - for elem in sf.shape: - if _IDENT_RE.fullmatch(elem) and elem not in global_dims: - sibling = rec.fields.get(elem) - if isinstance(sibling, Integer) and sibling.dimension is None: - to_mark.add(elem) - return to_mark - - def _mark(fields: dict) -> dict: - result = {} - for name, f in fields.items(): - if isinstance(f, Array) and f.dtype == "string" and name in string_provider_names: - f = f.model_copy(update={"dimension": "component"}) - elif isinstance(f, Record): - local_dims = _record_local_dims(f) - new_fields = _mark(f.fields) - if local_dims: - new_fields = { - fn: ( - sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf - ) - for fn, sf in new_fields.items() - } - f = f.model_copy(update={"fields": new_fields}) - elif isinstance(f, Union): - f = f.model_copy(update={"arms": _mark(f.arms)}) - elif isinstance(f, List): - item = f.item - new_item: Record | Union - if isinstance(item, Record): - local_dims = _record_local_dims(item) - new_item_fields = _mark(item.fields) - if local_dims: - new_item_fields = { - fn: ( - sf.model_copy(update={"dimension": "record"}) - if fn in local_dims and isinstance(sf, Integer) - else sf - ) - for fn, sf in new_item_fields.items() - } - new_item = item.model_copy(update={"fields": new_item_fields}) - else: - new_item = item.model_copy(update={"arms": _mark(item.arms)}) - f = f.model_copy(update={"item": new_item}) - result[name] = f - return result - - return {bn: _mark(bf) for bn, bf in blocks.items()} - - @staticmethod - def _infer_fk_from_shapes(blocks: dict[str, dict]) -> dict[str, dict]: - """Post-pass: infer fk= and pk= from resolved lookup shape elements.""" - _lookup_re = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") - - fk_map: dict[tuple[str, str], str] = {} - pk_set: set[tuple[str, str]] = set() - - def _scan_record(rec: Record, block_name: str) -> None: - for sf in rec.fields.values(): - if isinstance(sf, Array): - for elem in sf.shape: - m = _lookup_re.fullmatch(elem) - if m: - pk_block, _col, fk_fname = m.groups() - sibling = rec.fields.get(fk_fname) - if sibling is not None and getattr(sibling, "fk", None) is None: - fk_map[(block_name, fk_fname)] = f"{pk_block}.{fk_fname}" - pk_set.add((pk_block, fk_fname)) - - def _scan(fields: dict, block_name: str) -> None: - for f in fields.values(): - if isinstance(f, Record): - _scan_record(f, block_name) - elif isinstance(f, Union): - _scan(f.arms, block_name) - elif isinstance(f, List): - item = f.item - if isinstance(item, Record): - _scan_record(item, block_name) - elif isinstance(item, Union): - _scan(item.arms, block_name) - - for block_name, block_fields in blocks.items(): - _scan(block_fields, block_name) - - if not fk_map and not pk_set: - return blocks - - def _apply_record(rec: Record, block_name: str) -> Record: - updates: dict = {} - for fname, sf in rec.fields.items(): - updated = sf - if (block_name, fname) in fk_map and getattr(sf, "fk", None) is None: - updated = updated.model_copy(update={"fk": fk_map[(block_name, fname)]}) - if (block_name, fname) in pk_set and not getattr(sf, "pk", False): - updated = updated.model_copy(update={"pk": True}) - if updated is not sf: - updates[fname] = updated - if not updates: - return rec - return rec.model_copy( - update={"fields": {fn: updates.get(fn, sf) for fn, sf in rec.fields.items()}} - ) - - def _apply(fields: dict, block_name: str) -> dict: - result = {} - for name, f in fields.items(): - if isinstance(f, Record): - f = _apply_record(f, block_name) - elif isinstance(f, Union): - f = f.model_copy(update={"arms": _apply(f.arms, block_name)}) - elif isinstance(f, List): - item = f.item - new_item: Record | Union - if isinstance(item, Record): - new_item = _apply_record(item, block_name) - else: - new_item = item.model_copy(update={"arms": _apply(item.arms, block_name)}) - f = f.model_copy(update={"item": new_item}) - result[name] = f - return result - - return {bn: _apply(bf, bn) for bn, bf in blocks.items()} - - @staticmethod - def map_blocks(dfn: Dfn) -> dict[str, dict]: - """ - Convert all v1 fields in a Dfn to v2 types and return a block dict. - - Three phases: - 1. Field conversion (map_field per top-level field). - 2. Dimension annotation (_mark_dimension_fields). - 3. FK/PK inference (_infer_fk_from_shapes). - """ - all_v1 = cast(OMD, dfn.fields) - grouped: dict[str, dict] = {} - for v1_field in all_v1.values(multi=True): - if v1_field.in_record: # type: ignore[attr-defined] - continue - block_name = v1_field.block - mapped = MapV1To2.map_field(dfn, v1_field) - grouped.setdefault(block_name, {})[v1_field.name] = mapped - - blocks: dict[str, dict] = {} - if period := grouped.pop("period", None): - blocks["period"] = MapV1To2.map_period_block(dfn, period) - for block_name, block_data in grouped.items(): - blocks[block_name] = block_data - - blocks = MapV1To2._mark_dimension_fields(blocks) - return MapV1To2._infer_fk_from_shapes(blocks) - - @staticmethod - def to_component(dfn: Dfn) -> Any: - """ - Convert a Dfn to the appropriate Component (Simulation, Model, or Package). - - For v1-mapped Dfns, variant_of is inferred from the component name: names - ending in "g" (grid variant) or "a" (array variant) are treated as variants - of the same name without the suffix (e.g. "gwf-welg" → "gwf-wel"). - """ - from modflow_devtools.dfns.schema.v2 import ( - Block, - Model, - Package, - Simulation, - ) - - name = dfn.name - blocks: dict[str, Block] | None = None - if dfn.blocks: - blocks = { - block_name: Block( - name=block_name, - fields={k: v for k, v in block_fields.items() if isinstance(v, FieldBase)}, # type: ignore[misc] - ) - for block_name, block_fields in dfn.blocks.items() - if isinstance(block_fields, dict) - } - - def _infer_variant_of(n: str) -> str | None: - if n.endswith(("g", "a")): - return n[:-1] - return None - - common: dict[str, Any] = { - "name": name, - "blocks": blocks, - "parent": dfn.parent, - "schema_version": dfn.schema_version, - } - if name == "sim-nam": - return Simulation(**common) - if name.endswith("-nam"): - return Model(**common) - if name.startswith("sln-"): - return Package(**common, subtype="solution", multi=dfn.multi) - if name.startswith("exg-"): - return Package(**common, subtype="exchange", multi=dfn.multi) - if name.startswith("utl-"): - return Package( - **common, - subtype="utility", - multi=dfn.multi, - variant_of=_infer_variant_of(name), - ) - has_period = bool(blocks and any("period" in k for k in blocks)) - subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = ( - "advanced" if dfn.advanced else "stress" if has_period else None - ) - return Package( - **common, - subtype=subtype, - multi=dfn.multi, - variant_of=_infer_variant_of(name), - ) - - def map(self, dfn: Dfn) -> Any: - """Map a v1 (or v2) Dfn to a v2 Component.""" - if dfn.schema_version == Version("2"): - return MapV1To2.to_component(dfn) - mapped_blocks = MapV1To2.map_blocks(dfn) - temp = dataclasses.replace(dfn, schema_version=Version("2"), blocks=mapped_blocks) - return MapV1To2.to_component(temp) - - -def map( - dfn: Dfn, - schema_version: str | Version = "2", -) -> Any: - """Map a MODFLOW 6 definition to v2 schema.""" - version = Version(str(schema_version)) - if version == Version("2"): - return MapV1To2().map(dfn) - raise ValueError(f"Unsupported schema version: {schema_version!r}. Expected '2'.") + return __map_field(field) + + name = dfn["name"] + blocks: dict[str, v2.Block] = {} + + for field in fields.values(multi=True): + if field["in_record"]: # type: ignore[attr-defined] + continue # record subfields are handled recursively + v2_field = _map_field(field) + blocks.setdefault(field["block"], v2.Block(name=field["block"], fields={})).fields[ + field["name"] + ] = v2_field + blocks[field["block"]].repeats = field.get("block_variable", False) + + blocks = _resolve_dimensions(blocks) + blocks = _resolve_relations(blocks) + + d: dict[str, Any] = { + "schema_version": "2", + "name": name, + "parent": dfn["parent"], + "blocks": blocks or None, + } + if name == "sim-nam": + return v2.Simulation(**d) + if name.endswith("-nam"): + return v2.Model(**d) + + subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = None + if name.startswith("sln-"): + subtype = "solution" + elif name.startswith("exg-"): + subtype = "exchange" + elif name.startswith("utl-"): + subtype = "utility" + else: + is_stress_pkg = bool(any(blocks) and any("period" in k for k in blocks)) + subtype = "advanced" if dfn["advanced"] else "stress" if is_stress_pkg else None + return v2.Package( + **d, + subtype=subtype, + multi=dfn["multi"], + ) diff --git a/modflow_devtools/dfns/registry.py b/modflow_devtools/dfns/registry.py index a19a6c0c..33a21fc4 100644 --- a/modflow_devtools/dfns/registry.py +++ b/modflow_devtools/dfns/registry.py @@ -1,380 +1,55 @@ -""" -DFN registry infrastructure for discovery, caching, and synchronization. - -This module provides: -- Pydantic schemas for registry and bootstrap configuration -- Cache management for registries and DFN files -- Registry classes for local and remote DFN access -""" - from __future__ import annotations import os -import sys -from datetime import datetime +import tempfile from os import PathLike from pathlib import Path -from typing import TYPE_CHECKING - -from packaging.version import Version -from pydantic import BaseModel, Field +from platform import system -if TYPE_CHECKING: - import pooch +import pooch +import tomli +from pydantic import BaseModel, Field, PrivateAttr - from modflow_devtools.dfns import DfnSpec - from modflow_devtools.dfns.schema.v2 import Component +from modflow_devtools.dfns.schema import Dfns __all__ = [ - "BootstrapConfig", "DfnRegistry", - "DfnRegistryDiscoveryError", - "DfnRegistryError", - "DfnRegistryFile", - "DfnRegistryMeta", - "DfnRegistryNotFoundError", "LocalDfnRegistry", "RemoteDfnRegistry", - "SourceConfig", - "get_bootstrap_config", - "get_cache_dir", - "get_registry", - "get_sync_status", - "get_user_config_path", - "sync_dfns", + "is_cached", ] -# ============================================================================= -# Pydantic Schemas for Bootstrap Configuration -# ============================================================================= - - -class SourceConfig(BaseModel): - """Configuration for a DFN source repository.""" - - repo: str = Field(description="GitHub repository identifier (owner/name)") - dfn_path: str = Field( - default="doc/mf6io/mf6ivar/dfn", - description="Path within the repository to the DFN files directory", - ) - registry_path: str = Field( - default=".registry/dfns.toml", - description="Path within the repository to the registry metadata file", - ) - refs: list[str] = Field( - default_factory=list, - description="Git refs (branches, tags, commit hashes) to sync by default", - ) - - -class BootstrapConfig(BaseModel): - """Bootstrap configuration for DFN sources.""" - - sources: dict[str, SourceConfig] = Field( - default_factory=dict, - description="Map of source names to their configurations", - ) - - @classmethod - def load(cls, path: str | PathLike) -> BootstrapConfig: - """Load bootstrap configuration from a TOML file.""" - import tomli - - path = Path(path) - if not path.exists(): - return cls() - - with path.open("rb") as f: - data = tomli.load(f) - - # Convert sources dict to SourceConfig instances - sources = {} - for name, config in data.get("sources", {}).items(): - sources[name] = SourceConfig(**config) - - return cls(sources=sources) - - @classmethod - def merge(cls, base: BootstrapConfig, overlay: BootstrapConfig) -> BootstrapConfig: - """Merge two bootstrap configs, with overlay taking precedence.""" - merged_sources = dict(base.sources) - merged_sources.update(overlay.sources) - return cls(sources=merged_sources) - - -# ============================================================================= -# Pydantic Schemas for Registry Files -# ============================================================================= - - -class DfnRegistryFile(BaseModel): - """Entry for a single file in the registry.""" - - hash: str = Field(description="SHA256 hash of the file (sha256:...)") - - -class DfnRegistryMeta(BaseModel): - """ - Registry metadata and file listings. - - This represents the contents of a dfns.toml registry file. - """ - - schema_version: str = Field( - default="1.0", - description="Registry schema version", - ) - generated_at: datetime | None = Field( - default=None, - description="When the registry was generated", - ) - devtools_version: str | None = Field( - default=None, - description="Version of modflow-devtools that generated this registry", - ) - ref: str | None = Field( - default=None, - description="Git ref this registry was generated from", - ) - files: dict[str, DfnRegistryFile] = Field( - default_factory=dict, - description="Map of filenames to file metadata", - ) - - @classmethod - def load(cls, path: str | PathLike) -> DfnRegistryMeta: - """Load registry metadata from a TOML file.""" - import tomli - - path = Path(path) - with path.open("rb") as f: - data = tomli.load(f) - - # Handle nested structure: files section contains filename -> {hash: ...} - files_data = data.pop("files", {}) - files = {} - for filename, file_info in files_data.items(): - if isinstance(file_info, dict): - files[filename] = DfnRegistryFile(**file_info) - elif isinstance(file_info, str): - # Support shorthand: filename = "hash" - files[filename] = DfnRegistryFile(hash=file_info) - - # Handle metadata section if present - metadata = data.pop("metadata", {}) - ref = metadata.get("ref") or data.pop("ref", None) - - return cls( - schema_version=data.get("schema_version", "1.0"), - generated_at=data.get("generated_at"), - devtools_version=data.get("devtools_version"), - ref=ref, - files=files, - ) - - def save(self, path: str | PathLike) -> None: - """Save registry metadata to a TOML file.""" - import tomli_w - - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) - - data: dict = { - "schema_version": self.schema_version, - } - - if self.generated_at: - data["generated_at"] = self.generated_at.isoformat() - if self.devtools_version: - data["devtools_version"] = self.devtools_version - - if self.ref: - data["metadata"] = {"ref": self.ref} - - # Write files section - data["files"] = { - filename: {"hash": file_info.hash} for filename, file_info in self.files.items() - } - - with path.open("wb") as f: - tomli_w.dump(data, f) - - -# ============================================================================= -# Cache and Configuration Utilities -# ============================================================================= - - -def get_user_config_path(subdir: str = "dfn") -> Path: - """ - Get the user configuration directory path. - - Parameters - ---------- - subdir : str - Subdirectory name (e.g., "dfn", "models", "programs"). - - Returns - ------- - Path - Path to user config file (e.g., ~/.config/modflow-devtools/dfns.toml). - """ - if sys.platform == "win32": - base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) - else: - base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) - - return base / "modflow-devtools" / f"{subdir}s.toml" - - -def get_cache_dir(subdir: str = "dfn") -> Path: - """ - Get the cache directory path. - - Parameters - ---------- - subdir : str - Subdirectory name (e.g., "dfn", "models", "programs"). - - Returns - ------- - Path - Path to cache directory (e.g., ~/.cache/modflow-devtools/dfn/). - """ - if sys.platform == "win32": - base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) - else: - base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) - - return base / "modflow-devtools" / subdir - - -def get_bootstrap_config() -> BootstrapConfig: - """ - Load and merge bootstrap configuration. - - Loads the bundled bootstrap file and merges with user config if present. - - Returns - ------- - BootstrapConfig - Merged bootstrap configuration. - """ - # Load bundled bootstrap config - bundled_path = Path(__file__).parent / "dfns.toml" - bundled_config = BootstrapConfig.load(bundled_path) - - # Load user config if present - user_path = get_user_config_path("dfn") - if user_path.exists(): - user_config = BootstrapConfig.load(user_path) - return BootstrapConfig.merge(bundled_config, user_config) - - return bundled_config - - -# ============================================================================= -# Registry Classes -# ============================================================================= - - class DfnRegistry(BaseModel): - """ - Base class for DFN registries. - - A registry provides access to DFN files and the parsed DfnSpec. - This is a Pydantic model that can be used directly for data-only use cases. - """ + """Base class for DFN registries.""" model_config = {"arbitrary_types_allowed": True} - source: str = Field(default="modflow6", description="Source repository name") - ref: str = Field(default="develop", description="Git ref (branch, tag, or commit hash)") - - _spec: DfnSpec | None = None + _spec: Dfns | None = PrivateAttr(default=None, init=False) @property - def spec(self) -> DfnSpec: - """ - Get the full DFN specification. + def spec(self) -> Dfns: + raise NotImplementedError - Returns - ------- - DfnSpec - The parsed specification with hierarchical structure. - """ - raise NotImplementedError("Subclasses must implement spec property") - - @property - def schema_version(self) -> Version: - """Get the schema version of the specification.""" - return self.spec.schema_version - - @property - def components(self) -> dict[str, Component]: - """Get all components as a flat dictionary.""" - return dict(self.spec.components) - - def get_dfn(self, component: str) -> Component: - """ - Get a component definition by name. - - Parameters - ---------- - component : str - Component name (e.g., "gwf-chd", "sim-nam"). - - Returns - ------- - Component - The requested component definition. - """ - return self.spec.components[component] - - def get_dfn_path(self, component: str) -> Path: - """ - Get the local file path for a DFN. - - Parameters - ---------- - component : str - Component name (e.g., "gwf-chd", "sim-nam"). - - Returns - ------- - Path - Path to the local DFN file. - """ - raise NotImplementedError("Subclasses must implement get_dfn_path") + def get_path(self, component: str) -> Path: + raise NotImplementedError class LocalDfnRegistry(DfnRegistry): - """ - Registry for local DFN files. - - Use this for working with DFN files on the local filesystem, - e.g., during development or with a local clone of the MODFLOW 6 repository. - """ + """Registry for local DFN files.""" path: Path = Field(description="Path to directory containing DFN files") - def model_post_init(self, __context) -> None: - """Validate and resolve path after initialization.""" + def model_post_init(self, _) -> None: object.__setattr__(self, "path", Path(self.path).expanduser().resolve()) @property - def spec(self) -> DfnSpec: - """Load and return the DFN specification from local files.""" + def spec(self) -> Dfns: if self._spec is None: - from modflow_devtools.dfns import DfnSpec - - self._spec = DfnSpec.load(self.path) + self._spec = Dfns.load(self.path) return self._spec - def get_dfn_path(self, component: str) -> Path: - """Get the local file path for a DFN component.""" - # Look for both .dfn and .toml extensions + def get_path(self, component: str) -> Path: for ext in [".dfn", ".toml"]: p = self.path / f"{component}{ext}" if p.exists(): @@ -382,410 +57,139 @@ def get_dfn_path(self, component: str) -> Path: raise FileNotFoundError(f"Component '{component}' not found in {self.path}") +def _auto_sync() -> bool: + return os.environ.get("MODFLOW_DEVTOOLS_AUTO_SYNC", "").lower() in ("1", "true", "yes") + + class RemoteDfnRegistry(DfnRegistry): - """ - Registry for remote DFN files with Pooch-based caching. - - Handles remote registry discovery, caching, and DFN file fetching. - URLs are constructed dynamically from bootstrap metadata, or can be - overridden by providing explicit repo/dfn_path/registry_path values. - - Examples - -------- - >>> # Use bootstrap config - >>> registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - >>> dfn = registry.get_dfn("gwf-chd") - - >>> # Override repo directly (useful for testing) - >>> registry = RemoteDfnRegistry( - ... source="modflow6", - ... ref="registry", - ... repo="wpbonelli/modflow6", - ... ) - """ + """Registry for DFN files associated with a GitHub repository release.""" - # Optional overrides (bypass bootstrap config when provided) - repo: str | None = Field( - default=None, - description="GitHub repository (owner/repo). Overrides bootstrap config.", - ) - dfn_path: str | None = Field( - default=None, - description="Path to DFN files in repo. Overrides bootstrap config.", + release_id: str = Field( + description="DFN source repository release ID (owner/name@tag)", ) - registry_path: str | None = Field( - default=None, - description="Path to registry file in repo. Overrides bootstrap config.", - ) - - _registry_meta: DfnRegistryMeta | None = None - _source_config: SourceConfig | None = None - _pooch: pooch.Pooch | None = None - _files_dir: Path | None = None - - def model_post_init(self, __context) -> None: - """Initialize registry after model creation.""" - self._ensure_source_config() - - def _ensure_source_config(self) -> SourceConfig: - """Load and cache source configuration from bootstrap or overrides.""" - if self._source_config is None: - # If repo is provided, construct config from overrides - if self.repo is not None: - self._source_config = SourceConfig( - repo=self.repo, - dfn_path=self.dfn_path or "doc/mf6io/mf6ivar/dfn", - registry_path=self.registry_path or ".registry/dfns.toml", - refs=[self.ref], - ) - else: - # Load from bootstrap config - config = get_bootstrap_config() - if self.source not in config.sources: - raise ValueError( - f"Unknown source '{self.source}'. " - f"Available sources: {list(config.sources.keys())}" - ) - self._source_config = config.sources[self.source] - return self._source_config - - def _get_registry_cache_path(self) -> Path: - """Get path to cached registry file.""" - cache_dir = get_cache_dir("dfn") - return cache_dir / "registries" / self.source / self.ref / "dfns.toml" - - def _get_files_cache_dir(self) -> Path: - """Get directory for cached DFN files.""" - cache_dir = get_cache_dir("dfn") - return cache_dir / "files" / self.source / self.ref - - def _construct_raw_url(self, path: str) -> str: - """Construct GitHub raw content URL for a file.""" - source_config = self._ensure_source_config() - return f"https://raw.githubusercontent.com/{source_config.repo}/{self.ref}/{path}" - - def _fetch_registry(self, force: bool = False) -> DfnRegistryMeta: - """Fetch registry metadata from remote or cache.""" - cache_path = self._get_registry_cache_path() - - # Use cached registry if available and not forcing refresh - if cache_path.exists() and not force: - return DfnRegistryMeta.load(cache_path) - - # Fetch from remote - source_config = self._ensure_source_config() - registry_url = self._construct_raw_url(source_config.registry_path) - - import urllib.error - import urllib.request - - try: - with urllib.request.urlopen(registry_url, timeout=30) as response: - content = response.read() - except urllib.error.HTTPError as e: - if e.code == 404: - raise DfnRegistryNotFoundError( - f"Registry not found at {registry_url} for '{self.source}@{self.ref}'. " - f"The registry file may not exist for this ref." - ) from e - raise DfnRegistryDiscoveryError( - f"Failed to fetch registry from {registry_url}: {e}" - ) from e - except urllib.error.URLError as e: - raise DfnRegistryDiscoveryError( - f"Network error fetching registry from {registry_url}: {e}" - ) from e - - # Parse and cache - import tomli - - data = tomli.loads(content.decode("utf-8")) - - # Build registry meta from parsed data - files_data = data.pop("files", {}) - files = {} - for filename, file_info in files_data.items(): - if isinstance(file_info, dict): - files[filename] = DfnRegistryFile(**file_info) - elif isinstance(file_info, str): - files[filename] = DfnRegistryFile(hash=file_info) - - metadata = data.pop("metadata", {}) - registry_meta = DfnRegistryMeta( - schema_version=data.get("schema_version", "1.0"), - generated_at=data.get("generated_at"), - devtools_version=data.get("devtools_version"), - ref=metadata.get("ref") or data.get("ref") or self.ref, - files=files, - ) - - # Cache the registry - cache_path.parent.mkdir(parents=True, exist_ok=True) - registry_meta.save(cache_path) - - return registry_meta - - def _ensure_registry_meta(self, force: bool = False) -> DfnRegistryMeta: - """Ensure registry metadata is loaded.""" - if self._registry_meta is None or force: - self._registry_meta = self._fetch_registry(force=force) - return self._registry_meta - - def _setup_pooch(self) -> pooch.Pooch: - """Set up Pooch for DFN file fetching.""" - if self._pooch is not None: - return self._pooch - - import pooch - - registry_meta = self._ensure_registry_meta() - source_config = self._ensure_source_config() - - # Construct base URL for DFN files - base_url = self._construct_raw_url(source_config.dfn_path) + "/" - - # Build registry dict for Pooch (filename -> hash) - pooch_registry = {} - for filename, file_info in registry_meta.files.items(): - # Pooch expects hash without "sha256:" prefix for sha256 - hash_value = file_info.hash - if hash_value.startswith("sha256:"): - hash_value = hash_value[7:] - pooch_registry[filename] = f"sha256:{hash_value}" - - self._files_dir = self._get_files_cache_dir() - self._pooch = pooch.create( - path=self._files_dir, - base_url=base_url, - registry=pooch_registry, - ) - - return self._pooch - def sync(self, force: bool = False) -> None: + @staticmethod + def base_cache_path() -> Path: """ - Synchronize registry and optionally pre-fetch all DFN files. - - Parameters - ---------- - force : bool, optional - If True, re-fetch registry even if cached. Default is False. + Get the base DFN cache path. On Unix: $XDG_CACHE_HOME/modflow-devtools/dfns, + falling back to ~/.cache/. On Windows: %LOCALAPPDATA%/modflow-devtools/dfns. """ - self._ensure_registry_meta(force=force) - self._setup_pooch() - - @property - def registry_meta(self) -> DfnRegistryMeta: - """Get the registry metadata.""" - return self._ensure_registry_meta() - - @property - def spec(self) -> DfnSpec: - """Load and return the DFN specification from cached files.""" - if self._spec is None: - from modflow_devtools.dfns import DfnSpec - - # Ensure all files are fetched - self._fetch_all_files() - - # Load from cache directory - self._spec = DfnSpec.load(self._get_files_cache_dir()) - return self._spec - - def _fetch_all_files(self) -> None: - """Fetch all DFN files to cache.""" - p = self._setup_pooch() - registry_meta = self._ensure_registry_meta() - - for filename in registry_meta.files: - # Skip non-DFN files (like spec.toml) - if filename.endswith(".dfn") or filename.endswith(".toml"): - p.fetch(filename) - - def get_dfn_path(self, component: str) -> Path: - """Get the local cached file path for a DFN component.""" - p = self._setup_pooch() - registry_meta = self._ensure_registry_meta() - - # Look for both .dfn and .toml extensions - for ext in [".dfn", ".toml"]: - filename = f"{component}{ext}" - if filename in registry_meta.files: - return Path(p.fetch(filename)) - - raise FileNotFoundError( - f"Component '{component}' not found in registry for '{self.source}@{self.ref}'" - ) - - -# ============================================================================= -# Exceptions -# ============================================================================= - - -class DfnRegistryError(Exception): - """Base exception for DFN registry errors.""" - - pass - - -class DfnRegistryNotFoundError(DfnRegistryError): - """Registry file not found for the specified ref.""" - - pass + if system() == "Windows": + base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) + else: + base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) + return base / "modflow-devtools" / "dfns" + + @staticmethod + def user_config_path() -> Path: + """ + Path to the user overlay configuration file, in which users can override + and/or add to the configuration shipped with the package. + On Unix: $XDG_CONFIG_HOME/modflow-devtools/dfns.toml (default ~/.config/). + On Windows: %APPDATA%/modflow-devtools/dfns.toml. + """ + if system() == "Windows": + base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) + else: + base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) + return base / "modflow-devtools" / "dfns.toml" -class DfnRegistryDiscoveryError(DfnRegistryError): - """Error during registry discovery.""" + @classmethod + def from_ids(cls, *ids: str) -> dict[str, RemoteDfnRegistry]: + """Create registries from one or more DFN source repository release IDs.""" + registries = {} - pass + for id in ids: + registry = cls(release_id=id) + if _auto_sync() and ( + not registry.cache_path.exists() or not any(registry.cache_path.iterdir()) + ): + registry.sync() -# ============================================================================= -# Sync Functions -# ============================================================================= + registries[id] = registry + return registries -def sync_dfns( - source: str = "modflow6", - ref: str | None = None, - force: bool = False, -) -> list[RemoteDfnRegistry]: - """ - Synchronize DFN registries from remote sources. - - Parameters - ---------- - source : str, optional - Source repository name. Default is "modflow6". - ref : str, optional - Specific git ref to sync. If not provided, syncs all configured refs. - force : bool, optional - If True, re-fetch registries even if cached. Default is False. - - Returns - ------- - list[RemoteDfnRegistry] - List of synced registries. - - Examples - -------- - >>> # Sync all configured refs - >>> registries = sync_dfns() - - >>> # Sync specific ref - >>> registries = sync_dfns(ref="6.6.0") - - >>> # Force re-sync - >>> registries = sync_dfns(force=True) - """ - config = get_bootstrap_config() + @classmethod + def load(cls, path: str | PathLike) -> dict[str, RemoteDfnRegistry]: + """Load registries from a TOML file of DFN source repository release IDs.""" + path = Path(path) + if not path.exists(): + return {} - if source not in config.sources: - raise ValueError( - f"Unknown source '{source}'. Available sources: {list(config.sources.keys())}" - ) + with path.open("rb") as f: + data = tomli.load(f) - source_config = config.sources[source] + registries = {} + for id in data.get("releases", []): + registry = RemoteDfnRegistry(release_id=id) + registries[id] = registry - # Determine which refs to sync - refs_to_sync = [ref] if ref else source_config.refs + return registries - registries = [] - for r in refs_to_sync: - registry = RemoteDfnRegistry(source=source, ref=r) - registry.sync(force=force) - registries.append(registry) + @classmethod + def load_default(cls) -> dict[str, RemoteDfnRegistry]: + """ + Load registries from remote DFN source repository configuration bundled + with the package, and from a user overlay configuration file if present. + """ + base = RemoteDfnRegistry.load(Path(__file__).parent / "dfns.toml") + if not RemoteDfnRegistry.user_config_path().exists(): + return base - return registries + user = RemoteDfnRegistry.load(RemoteDfnRegistry.user_config_path()) + return base | user + @property + def cache_path(self) -> Path: + repo, tag = self.release_id.split("@") + return RemoteDfnRegistry.base_cache_path() / repo / tag -def get_sync_status(source: str = "modflow6") -> dict[str, bool]: - """ - Check which refs have cached registries. + @property + def spec(self) -> Dfns: + if self._spec is None: + if not self.cache_path.exists() or not any(self.cache_path.iterdir()): + self.sync() + self._spec = Dfns.load(self.cache_path) + return self._spec - Parameters - ---------- - source : str, optional - Source repository name. Default is "modflow6". + def sync(self, force: bool = False) -> None: + """Download and extract DFN files for this release to the local cache.""" - Returns - ------- - dict[str, bool] - Map of ref names to whether they have a cached registry. - """ - config = get_bootstrap_config() + if not force and self.cache_path.exists() and any(self.cache_path.iterdir()): + return - if source not in config.sources: - raise ValueError( - f"Unknown source '{source}'. Available sources: {list(config.sources.keys())}" - ) + asset_name = "dfns.zip" + repo, tag = self.release_id.split("@") + url = f"https://github.com/{repo}/releases/download/{tag}/{asset_name}" - source_config = config.sources[source] - cache_dir = get_cache_dir("dfn") + self.cache_path.mkdir(parents=True, exist_ok=True) - status = {} - for ref in source_config.refs: - registry_path = cache_dir / "registries" / source / ref / "dfns.toml" - status[ref] = registry_path.exists() + with tempfile.TemporaryDirectory() as tmpdir: + pooch.retrieve( + url=url, + known_hash=None, + path=tmpdir, + fname=asset_name, + processor=pooch.Unzip(extract_dir=str(self.cache_path)), + ) - return status + def get_path(self, component: str) -> Path: + if not self.cache_path.exists() or not any(self.cache_path.iterdir()): + self.sync() + for ext in [".dfn", ".toml"]: + p = self.cache_path / f"{component}{ext}" + if p.exists(): + return p + raise FileNotFoundError(f"Component '{component}' not found for '{self.release_id}'") -def get_registry( - source: str = "modflow6", - ref: str = "develop", - auto_sync: bool = False, - path: str | PathLike | None = None, -) -> DfnRegistry: +def is_cached(release_id: str) -> bool: """ - Get a registry for the specified source and ref. - - Parameters - ---------- - source : str, optional - Source repository name. Default is "modflow6". - ref : str, optional - Git ref (branch, tag, or commit hash). Default is "develop". - auto_sync : bool, optional - If True and registry is not cached, automatically sync. Default is False - (opt-in while experimental). Can be enabled via MODFLOW_DEVTOOLS_AUTO_SYNC - environment variable (set to "1", "true", or "yes"). - Ignored when path is provided. - path : str or PathLike, optional - Path to a local directory containing DFN files. If provided, returns - a LocalDfnRegistry for autodiscovery instead of RemoteDfnRegistry. - When using a local path, source and ref are used for metadata only. - - Returns - ------- - DfnRegistry - Registry for the specified source and ref. Returns LocalDfnRegistry - if path is provided, otherwise RemoteDfnRegistry. - - Examples - -------- - >>> # Remote registry (existing behavior) - >>> registry = get_registry(ref="6.6.0") - >>> dfn = registry.get_dfn("gwf-chd") - - >>> # Local registry with autodiscovery (NEW) - >>> registry = get_registry(path="/path/to/mf6/doc/mf6io/mf6ivar/dfn") - >>> dfn = registry.get_dfn("gwf-chd") + Check whether a remote DFN source repository's release is in the cache. """ - # If path is provided, return LocalDfnRegistry for autodiscovery - if path is not None: - return LocalDfnRegistry(path=Path(path), source=source, ref=ref) - - # Check for auto-sync opt-in (experimental - off by default) - if os.environ.get("MODFLOW_DEVTOOLS_AUTO_SYNC", "").lower() in ("1", "true", "yes"): - auto_sync = True - - registry = RemoteDfnRegistry(source=source, ref=ref) - - # Check if registry is cached - cache_path = registry._get_registry_cache_path() - if not cache_path.exists() and auto_sync: - registry.sync() - - return registry + repo, tag = release_id.split("@") + cache_dir = RemoteDfnRegistry.base_cache_path() / repo / tag + return any(cache_dir.iterdir()) if cache_dir.is_dir() else False diff --git a/modflow_devtools/dfns/schema/v2.py b/modflow_devtools/dfns/schema.py similarity index 76% rename from modflow_devtools/dfns/schema/v2.py rename to modflow_devtools/dfns/schema.py index 049a77c4..fe134994 100644 --- a/modflow_devtools/dfns/schema/v2.py +++ b/modflow_devtools/dfns/schema.py @@ -2,51 +2,48 @@ import re from collections.abc import Mapping from os import PathLike +from pathlib import Path from typing import Annotated, Any, Literal -from packaging.version import Version +import tomli from pydantic import ( BaseModel, - ConfigDict, - GetCoreSchemaHandler, + computed_field, field_validator, model_validator, ) from pydantic import ( Field as PydanticField, ) -from pydantic_core import core_schema class FieldBase(BaseModel): - model_config = ConfigDict(frozen=True) - @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": - type_ = d.get("type") + type_name = d.get("type") type_map: dict[str | None, type[FieldBase]] = { "keyword": Keyword, "string": String, "integer": Integer, "double": Double, - "path": Path, + "path": File, "array": Array, "record": Record, "union": Union, "list": List, } - target = type_map.get(type_) - if target is None: - raise ValueError(f"Unknown or missing field type: {type_!r}") + type_ = type_map.get(type_name) + if type_ is None: + raise ValueError(f"Unknown or missing field type: {type_name!r}") if strict: - extra = set(d.keys()) - set(target.model_fields.keys()) + extra = set(d.keys()) - set(type_.model_fields.keys()) if extra: raise ValueError(f"Unrecognized keys in field data: {extra}") - return target.model_validate(d) + return type_.model_validate(d) class Keyword(FieldBase): - type: Literal["keyword"] = "keyword" + type: Literal["keyword"] = PydanticField(default="keyword", frozen=True) name: str longname: str | None = None description: str | None = None @@ -57,7 +54,7 @@ class Keyword(FieldBase): class String(FieldBase): - type: Literal["string"] = "string" + type: Literal["string"] = PydanticField(default="string", frozen=True) name: str longname: str | None = None description: str | None = None @@ -75,7 +72,7 @@ class String(FieldBase): class Integer(FieldBase): - type: Literal["integer"] = "integer" + type: Literal["integer"] = PydanticField(default="integer", frozen=True) name: str longname: str | None = None description: str | None = None @@ -102,7 +99,7 @@ def _coerce_dimension(cls, v: Any) -> Any: class Double(FieldBase): - type: Literal["double"] = "double" + type: Literal["double"] = PydanticField(default="double", frozen=True) name: str longname: str | None = None description: str | None = None @@ -114,8 +111,8 @@ class Double(FieldBase): time_series: bool = False -class Path(FieldBase): - type: Literal["path"] = "path" +class File(FieldBase): + type: Literal["file"] = PydanticField(default="file", frozen=True) name: str longname: str | None = None description: str | None = None @@ -126,13 +123,13 @@ class Path(FieldBase): Scalar = Annotated[ - Keyword | String | Integer | Double | Path, + Keyword | String | Integer | Double | File, PydanticField(discriminator="type"), ] class Array(FieldBase): - type: Literal["array"] = "array" + type: Literal["array"] = PydanticField(default="array", frozen=True) name: str longname: str | None = None description: str | None = None @@ -144,7 +141,7 @@ class Array(FieldBase): shape: list[str] = [] time_series: bool = False repeat: str | None = None - dimension: Literal["record", "component", "model", "simulation"] | None = None + dimension: Literal["component", "model", "simulation"] | None = None @field_validator("dimension", mode="before") @classmethod @@ -157,13 +154,17 @@ def _coerce_dimension(cls, v: Any) -> Any: @model_validator(mode="after") def _validate_dimension(self) -> "Array": - if self.dimension is not None and self.dtype != "string": - raise ValueError(f"Array {self.name!r}: dimension may only be set when dtype='string'") + if self.dimension is not None and self.shape: + raise ValueError( + f"Array {self.name!r}: the 'dimension' attribute may only " + "be set on self-sizing arrays (i.e., arrays whose 'shape' " + "is empty)" + ) return self class Record(FieldBase): - type: Literal["record"] = "record" + type: Literal["record"] = PydanticField(default="record", frozen=True) name: str longname: str | None = None description: str | None = None @@ -178,7 +179,7 @@ def children(self) -> "dict[str, Field]": class Union(FieldBase): - type: Literal["union"] = "union" + type: Literal["union"] = PydanticField(default="union", frozen=True) name: str longname: str | None = None description: str | None = None @@ -193,7 +194,7 @@ def children(self) -> "dict[str, Field]": class List(FieldBase): - type: Literal["list"] = "list" + type: Literal["list"] = PydanticField(default="list", frozen=True) name: str longname: str | None = None description: str | None = None @@ -209,44 +210,33 @@ def children(self) -> "dict[str, Field]": Field = Annotated[ - Keyword | String | Integer | Double | Path | Array | Record | Union | List, + Keyword | String | Integer | Double | File | Array | Record | Union | List, PydanticField(discriminator="type"), ] -# Backward-compat alias: all concrete v2 field types are FieldBase subclasses, -# so isinstance(field, FieldV2) remains True for any v2 field instance. -FieldV2 = FieldBase Record.model_rebuild() Union.model_rebuild() List.model_rebuild() -# Fallback set of well-known grid dim names used by grid_dims_for and the v1 -# mapper (map_period_block). Once all dims in the v1 corpus carry explicit -# "model" scope, this constant becomes unnecessary and will be removed. -GRID_DIM_NAMESPACE: frozenset[str] = frozenset( - {"nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert"} -) +DIMENSION_SCOPES = ("component", "model", "simulation") def _collect_explicit_dims(component: "ComponentBase") -> set[str]: """ Gather all explicitly declared dimension names from a component. - Collects Integer fields with ``dimension=True`` and string Array fields - with ``dimension=True``, recursing into Records, Union arms, and List - item records at any nesting depth. + Collects integer or array fields with a ``dimension`` attribute, + recursing into Records, Union arms, and List items as necessary. """ dims: set[str] = set() - _GLOBAL_SCOPES = ("component", "model", "simulation") - def _scan(fields: "dict[str, Any]") -> None: for f in fields.values(): - if isinstance(f, Integer) and f.dimension in _GLOBAL_SCOPES: + if isinstance(f, Integer) and f.dimension in DIMENSION_SCOPES: dims.add(f.name) - elif isinstance(f, Array) and f.dtype == "string" and f.dimension in _GLOBAL_SCOPES: + elif isinstance(f, Array) and f.dimension in DIMENSION_SCOPES: dims.add(f.name) elif isinstance(f, Record): _scan(f.fields) @@ -391,43 +381,23 @@ def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> l class Block(BaseModel): - model_config = ConfigDict(frozen=True) name: str fields: dict[str, Field] repeats: bool = False - optional: bool = False - @field_validator("fields", mode="before") - @classmethod - def _coerce_field_instances(cls, v: Any) -> Any: - if isinstance(v, dict): - return { - k: (val.model_dump() if isinstance(val, FieldBase) else val) for k, val in v.items() - } - return v + @property + def optional(self) -> bool: + return all(f.optional for f in self.fields.values()) Blocks = Mapping[str, Block] -class _VersionPydanticAnnotation: - @classmethod - def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler): - return core_schema.no_info_plain_validator_function( - lambda v: Version(str(v)) if not isinstance(v, Version) else v, - serialization=core_schema.to_string_ser_schema(), - ) - - -VersionField = Annotated[Version, _VersionPydanticAnnotation] - - class ComponentBase(BaseModel): - model_config = ConfigDict(frozen=True) name: str blocks: dict[str, Block] | None = None parent: str | list[str] | None = None - schema_version: VersionField | None = None + schema_version: str | None = None derived_dims: dict[str, str] | None = None @@ -442,9 +412,8 @@ class Model(ComponentBase): class Package(ComponentBase): type: Literal["package"] = "package" - multi: bool = False + multi: bool = False # whether multiple instances per parent are allowed subtype: Literal["solution", "exchange", "stress", "advanced", "utility"] | None = None - variant_of: str | None = None Component = Annotated[ @@ -452,12 +421,13 @@ class Package(ComponentBase): PydanticField(discriminator="type"), ] -# Shape element patterns _DIM_RE = re.compile(r"^[A-Za-z_]\w*$") _LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") +_BOUND_RE = re.compile(r"^[<>]=?") +_ARITH_RE = re.compile(r"^([A-Za-z_]\w*)\s*[+-]\s*\d+$") -def _known_dims_for(spec: "DfnSpec", component_name: str) -> set[str]: +def _known_dims_for(spec: "Dfns", component_name: str) -> set[str]: """ Return the full set of dim names valid for shape references in a component. Scope chain (levels 1-3; level 4 is intra-record sibling, checked per-field): @@ -502,15 +472,33 @@ def _validate_shape_element( Raises ValueError on any violation. """ + # Advisory bound annotation prefix (<, >, <=, >=): strip it and validate the core identifier. + if bound_m := _BOUND_RE.match(element): + core = element[bound_m.end() :] + if not _DIM_RE.fullmatch(core): + raise ValueError( + f"Array {array_field.name!r} has invalid shape element {element!r}: " + f"must be a plain identifier after the bound operator" + ) + if core in known_dims: + return + if enclosing_record is not None: + sibling = enclosing_record.fields.get(core) + if isinstance(sibling, Integer) and sibling.dimension == "record": + return + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{core!r} does not resolve to a known dim (explicit, derived, or grid)" + ) + if _DIM_RE.fullmatch(element): if element in known_dims: return - # Per-row varying shape: a sibling field in the same enclosing record - # supplies the inline count for this row. Valid when the sibling has - # dimension="record" (or, for string Arrays, is a record-scoped dim). + # Per-row varying shape: a sibling Integer with dimension="record" supplies + # an inline count on the same line. if enclosing_record is not None: sibling = enclosing_record.fields.get(element) - if isinstance(sibling, (Integer, Array)) and sibling.dimension == "record": + if isinstance(sibling, Integer) and sibling.dimension == "record": return raise ValueError( f"Array {array_field.name!r} shape element {element!r} " @@ -574,14 +562,29 @@ def _validate_shape_element( ) return + if m := _ARITH_RE.fullmatch(element): + # Arithmetic offset: `dim [+-] integer` — validate the dim part only. + dim_name = m.group(1) + if dim_name in known_dims: + return + if enclosing_record is not None: + sibling = enclosing_record.fields.get(dim_name) + if isinstance(sibling, Integer) and sibling.dimension == "record": + return + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"{dim_name!r} does not resolve to a known dim " + f"(explicit, derived, or grid)" + ) + raise ValueError( f"Array {array_field.name!r} has invalid shape element {element!r}: " - f"must be a dim reference (^[A-Za-z_]\\w*$) or a row-level " - f"lookup (block.column(fk_field))" + f"must be a dim reference (^[A-Za-z_]\\w*$), an arithmetic offset " + f"(dim [+-] integer), or a row-level lookup (block.column(fk_field))" ) -def _validate_fk_fields(component: "ComponentBase", spec: "DfnSpec") -> None: +def _validate_fk_fields(component: "ComponentBase", spec: "Dfns") -> None: """ For every Integer/String field with fk or fk_ref set, validate structural consistency: @@ -636,7 +639,7 @@ def _check_fields(fields: dict) -> None: def _validate_array_shapes( component: "ComponentBase", component_name: str, - spec: "DfnSpec", + spec: "Dfns", ) -> None: """ Validate all Array.shape elements in a component. @@ -652,8 +655,18 @@ def _validate_array_shapes( known_dims = _known_dims_for(spec, component_name) def _check_array(arr: "Array", enclosing: "Record | None") -> None: - if arr.dtype == "string": - return # inline string arrays are self-sizing; no declared dim needed + if not arr.shape: + # Self-sizing (shape=[]) is valid at the top level and as the rightmost + # subfield of a record. The only invalid case is non-rightmost in a record: + # subsequent fields on the same line would be unreadable. + if enclosing is not None: + fields_list = list(enclosing.fields.keys()) + if not fields_list or fields_list[-1] != arr.name: + raise ValueError( + f"Array {arr.name!r}: only the rightmost field in a record may " + f"have an undeclared shape (self-sizing)" + ) + return # self-sizing: nothing to validate for elem in arr.shape: _validate_shape_element(elem, arr, component, enclosing, known_dims) @@ -675,31 +688,29 @@ def _check_array(arr: "Array", enclosing: "Record | None") -> None: _check_array(subfield, item) -class DfnSpec(BaseModel): - model_config = ConfigDict(frozen=True) - components: dict[str, Component] +class Dfns(BaseModel): + """A set of component definitions.""" - # ── Properties ─────────────────────────────────────────────────────────── + components: dict[str, Component] = PydanticField(default_factory=dict) + @computed_field @property - def schema_version(self) -> Version: + def schema_version(self) -> str: for c in self.components.values(): if c.schema_version is not None: return c.schema_version - return Version("2") + return "2" @property def root(self) -> "Simulation | None": - """Return the single Simulation component, or None if not present.""" + """The root (simulation) component, or None if not present.""" for c in self.components.values(): if isinstance(c, Simulation): return c return None - # ── Query helpers ───────────────────────────────────────────────────────── - def children_of(self, name: str) -> "dict[str, Component]": - """Return all components whose parent matches `name`.""" + """Return all components whose parent matches ``name``.""" return {n: c for n, c in self.components.items() if c.parent == name} def explicit_dims_for(self, component_name: str) -> set[str]: @@ -709,25 +720,28 @@ def explicit_dims_for(self, component_name: str) -> set[str]: def grid_dims_for(self, component_name: str) -> set[str]: """ Return dim names inherited by ``component_name`` from the rest of the spec. - - For v1-mapped specs (no explicit parent chain), this is a permissive - superset: it scans every other component for explicit dims (any scope - except "record") and derived dim names, then unions in - ``GRID_DIM_NAMESPACE`` as a fallback for dims not yet explicitly scoped - in the corpus. Native v2 specs with ``parent`` populated will - eventually use exact parent-chain resolution instead. """ - dims: set[str] = set(GRID_DIM_NAMESPACE) + dims: set[str] = set() for name, c in self.components.items(): if name != component_name: dims |= _collect_explicit_dims(c) dims |= set((c.derived_dims or {}).keys()) return dims - # ── Validation ──────────────────────────────────────────────────────────── + @model_validator(mode="after") + def _validate_schema_version_consistency(self) -> "Dfns": + versions = { + c.schema_version for c in self.components.values() if c.schema_version is not None + } + if len(versions) > 1: + raise ValueError( + f"All components must share the same schema_version; " + f"found: {sorted(str(v) for v in versions)}" + ) + return self @model_validator(mode="after") - def _validate_dims_and_shapes(self) -> "DfnSpec": + def _validate_dims_and_shapes(self) -> "Dfns": """ At construction time, for every component: 1. Validate derived_dims expressions (topological sort, operand scope). @@ -744,28 +758,25 @@ def _validate_dims_and_shapes(self) -> "DfnSpec": _validate_array_shapes(component, name, self) return self - # ── Loading ─────────────────────────────────────────────────────────────── - @classmethod - def load( - cls, - path: "str | PathLike", - schema_version: "str | Version | None" = None, - ) -> "DfnSpec": - """Load a DfnSpec from a directory of DFN or TOML files.""" - from pathlib import Path as _Path - - from modflow_devtools.dfn.mapper import _apply_parent_inference, load_flat - from modflow_devtools.dfns.mapper import map as map_dfn - - _path = _Path(path).expanduser().resolve() - dfns = load_flat(_path) - if not dfns: - raise ValueError(f"No DFN files found in {_path}") - - first = next(iter(dfns.values())) - if first.schema_version == Version("1"): - dfns = _apply_parent_inference(dfns) - - components: dict[str, Component] = {n: map_dfn(d, "2") for n, d in dfns.items()} - return cls(components=components) + def load(cls, path: str | PathLike) -> "Dfns": + """Load a directory of definition files.""" + from modflow_devtools.dfn import schema as v1 + from modflow_devtools.dfns.mapper import map as map_v2 + + dfns: dict = {} + path = Path(path).expanduser().resolve() + + dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.name not in v1.EXCLUDE_DFNS} + toml_paths = {p.stem: p for p in path.glob("*.toml") if p.name not in v1.EXCLUDE_DFNS} + + if dfn_paths: + dfns = v1.resolve_parents(v1.load_all(path)) + dfns = {n: map_v2(d) for n, d in dfns.items()} + if toml_paths: + for toml_path in toml_paths.values(): + with toml_path.open("rb") as f: + dfn = tomli.load(f) + dfns[dfn["name"]] = dfn + + return cls(components=dfns) diff --git a/modflow_devtools/models/__init__.py b/modflow_devtools/models/__init__.py index 00902c7e..5d212bcb 100644 --- a/modflow_devtools/models/__init__.py +++ b/modflow_devtools/models/__init__.py @@ -442,8 +442,8 @@ class DiscoveredModelRegistry: url: str -class ModelSourceRepo(BaseModel): - """A single source model repository in the bootstrap file.""" +class ModelSource(BaseModel): + """Model source repository configuration.""" @dataclass class SyncResult: @@ -462,18 +462,14 @@ class SyncStatus: cached_refs: list[str] missing_refs: list[str] - repo: str = Field(..., description="Repository in format 'owner/name'") name: str = Field( ..., description="Name for model addressing (injected from key if not explicit)" ) + repo: str = Field(..., description="Repository in format 'owner/name'") refs: list[str] = Field( default_factory=list, description="Default refs to sync (branches, tags, or commit hashes)", ) - registry_path: str = Field( - default=".registry", - description="Path to registry directory in repository", - ) @field_validator("repo") @classmethod @@ -599,9 +595,9 @@ def sync( if not refs: if verbose: print(f"No refs configured for source '{source_name}', aborting") - return ModelSourceRepo.SyncResult() + return ModelSource.SyncResult() - result = ModelSourceRepo.SyncResult() + result = ModelSource.SyncResult() for ref in refs: if not force and _DEFAULT_CACHE.has(source_name, ref): @@ -662,92 +658,41 @@ def list_synced_refs(self) -> list[str]: return [ref for source, ref in cached if source == self.name] -class ModelSourceConfig(BaseModel): - """Model source configuration file structure.""" +class ModelSources(BaseModel): + """Configuration for multiple model source repositories.""" - sources: dict[str, ModelSourceRepo] = Field( - ..., description="Map of source names to source metadata" + sources: dict[str, ModelSource] = Field( + default_factory=dict, description="Model source repositories" ) @classmethod def load( cls, - bootstrap_path: str | PathLike | None = None, - user_config_path: str | PathLike | None = None, - ) -> "ModelSourceConfig": - """ - Load model source configuration. - - Parameters - ---------- - bootstrap_path : str | PathLike | None - Path to bootstrap config file. If None, uses bundled default. - If provided, ONLY this file is loaded (no user config overlay unless specified). - user_config_path : str | PathLike | None - Path to user config file to overlay on top of bootstrap. - If None and bootstrap_path is None, attempts to load from default user config location. + path: str | PathLike | None = None, + ) -> "ModelSources": + """Load model source configurations from a TOML file.""" + path = Path(path) + if not path.exists(): + return cls() - Returns - ------- - ModelSourceConfig - Loaded and merged configuration - """ - # Load base config - if bootstrap_path is not None: - # Explicit bootstrap path - only load this file - with Path(bootstrap_path).open("rb") as f: - cfg = tomli.load(f) - else: - # Use bundled default - with _DEFAULT_CONFIG_PATH.open("rb") as f: - cfg = tomli.load(f) - - # If no explicit bootstrap path, try to load user config overlay - if user_config_path is None: - user_config_path = get_user_config_path() - - # Overlay user config if specified or found - if user_config_path is not None: - user_path = Path(user_config_path) - if user_path.exists(): - with user_path.open("rb") as f: - user_cfg = tomli.load(f) - # Merge user config sources into base config - if "sources" in user_cfg: - if "sources" not in cfg: - cfg["sources"] = {} - cfg["sources"] = cfg["sources"] | user_cfg["sources"] - - # inject source names if not explicitly provided - for name, src in cfg.get("sources", {}).items(): - if "name" not in src: - src["name"] = name - - return cls(**cfg) + with path.open("rb") as f: + data = tomli.load(f) - @classmethod - def merge(cls, base: "ModelSourceConfig", overlay: "ModelSourceConfig") -> "ModelSourceConfig": - """ - Merge two configurations, with overlay taking precedence. + sources = {} + for name, config in data.get("sources", {}).items(): + sources[name] = ModelSource(**config) - Parameters - ---------- - base : ModelSourceConfig - Base configuration - overlay : ModelSourceConfig - Configuration to overlay on top of base + return cls(sources=sources) - Returns - ------- - ModelSourceConfig - Merged configuration - """ - merged_sources = base.sources.copy() - merged_sources.update(overlay.sources) - return cls(sources=merged_sources) + @classmethod + def merge(cls, base: "ModelSources", overlay: "ModelSources") -> "ModelSources": + """Merge two source configurations. Overlay takes precedence.""" + merged = dict(base.sources) + merged.update(overlay.sources) + return cls(sources=merged) @property - def status(self) -> dict[str, ModelSourceRepo.SyncStatus]: + def status(self) -> dict[str, ModelSource.SyncStatus]: """ Sync status for all configured model source repositories. @@ -772,7 +717,7 @@ def status(self) -> dict[str, ModelSourceRepo.SyncStatus]: else: missing.append(ref) - status[name] = ModelSourceRepo.SyncStatus( + status[name] = ModelSource.SyncStatus( repo=source.repo, configured_refs=refs, cached_refs=cached, @@ -783,10 +728,10 @@ def status(self) -> dict[str, ModelSourceRepo.SyncStatus]: def sync( self, - source: str | ModelSourceRepo | None = None, + source: str | ModelSource | None = None, force: bool = False, verbose: bool = False, - ) -> dict[str, ModelSourceRepo.SyncResult]: + ) -> dict[str, ModelSource.SyncResult]: """ Synchronize registry files from model source(s). @@ -808,7 +753,7 @@ def sync( """ if source: - if isinstance(source, ModelSourceRepo): + if isinstance(source, ModelSource): if source.name not in self.sources: raise ValueError(f"Source '{source.name}' not found in bootstrap") sources = [source] @@ -1304,7 +1249,7 @@ def _try_best_effort_sync(): try: # Try to sync default refs (don't be verbose, don't fail on errors) - config = ModelSourceConfig.load() + config = ModelSources.load() config.sync(verbose=False) except Exception: # Silently fail - user will get clear error when trying to use registry diff --git a/modflow_devtools/models/__main__.py b/modflow_devtools/models/__main__.py index f5786317..e22a2c34 100644 --- a/modflow_devtools/models/__main__.py +++ b/modflow_devtools/models/__main__.py @@ -17,7 +17,7 @@ from . import ( _DEFAULT_CACHE, - ModelSourceConfig, + ModelSources, _try_best_effort_sync, ) @@ -47,7 +47,7 @@ def _format_grid(items, prefix=""): def cmd_sync(args): """Sync command handler.""" - config = ModelSourceConfig.load() + config = ModelSources.load() # If a specific source is provided, sync just that source if args.source: @@ -65,9 +65,9 @@ def cmd_sync(args): if source_obj is None: # If --repo is provided, create an ad-hoc source if args.repo: - from . import ModelSourceRepo + from . import ModelSource - source_obj = ModelSourceRepo( + source_obj = ModelSource( repo=args.repo, name=args.source, refs=[args.ref] if args.ref else [], @@ -111,7 +111,7 @@ def cmd_info(args): if os.environ.get("MODFLOW_DEVTOOLS_AUTO_SYNC", "").lower() in ("1", "true", "yes"): _try_best_effort_sync() - config = ModelSourceConfig.load() + config = ModelSources.load() status = config.status if not status: From 427b2f177449ccfa1c880daf23ea215b13b2968d Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Thu, 21 May 2026 04:10:55 -0700 Subject: [PATCH 12/29] revert models/programs api changes --- modflow_devtools/cli.py | 4 +- modflow_devtools/models/__init__.py | 121 +++++++++++++++++++------- modflow_devtools/models/__main__.py | 10 +-- modflow_devtools/programs/__init__.py | 2 +- 4 files changed, 96 insertions(+), 41 deletions(-) diff --git a/modflow_devtools/cli.py b/modflow_devtools/cli.py index b9d94da4..22978bd3 100644 --- a/modflow_devtools/cli.py +++ b/modflow_devtools/cli.py @@ -47,9 +47,9 @@ def _sync_all(): # Sync Models print("=== Models ===") try: - from modflow_devtools.models import ModelSources + from modflow_devtools.models import ModelSourceConfig - config = ModelSources.load() + config = ModelSourceConfig.load() config.sync() print("Models synced successfully") except Exception as e: diff --git a/modflow_devtools/models/__init__.py b/modflow_devtools/models/__init__.py index 5d212bcb..00902c7e 100644 --- a/modflow_devtools/models/__init__.py +++ b/modflow_devtools/models/__init__.py @@ -442,8 +442,8 @@ class DiscoveredModelRegistry: url: str -class ModelSource(BaseModel): - """Model source repository configuration.""" +class ModelSourceRepo(BaseModel): + """A single source model repository in the bootstrap file.""" @dataclass class SyncResult: @@ -462,14 +462,18 @@ class SyncStatus: cached_refs: list[str] missing_refs: list[str] + repo: str = Field(..., description="Repository in format 'owner/name'") name: str = Field( ..., description="Name for model addressing (injected from key if not explicit)" ) - repo: str = Field(..., description="Repository in format 'owner/name'") refs: list[str] = Field( default_factory=list, description="Default refs to sync (branches, tags, or commit hashes)", ) + registry_path: str = Field( + default=".registry", + description="Path to registry directory in repository", + ) @field_validator("repo") @classmethod @@ -595,9 +599,9 @@ def sync( if not refs: if verbose: print(f"No refs configured for source '{source_name}', aborting") - return ModelSource.SyncResult() + return ModelSourceRepo.SyncResult() - result = ModelSource.SyncResult() + result = ModelSourceRepo.SyncResult() for ref in refs: if not force and _DEFAULT_CACHE.has(source_name, ref): @@ -658,41 +662,92 @@ def list_synced_refs(self) -> list[str]: return [ref for source, ref in cached if source == self.name] -class ModelSources(BaseModel): - """Configuration for multiple model source repositories.""" +class ModelSourceConfig(BaseModel): + """Model source configuration file structure.""" - sources: dict[str, ModelSource] = Field( - default_factory=dict, description="Model source repositories" + sources: dict[str, ModelSourceRepo] = Field( + ..., description="Map of source names to source metadata" ) @classmethod def load( cls, - path: str | PathLike | None = None, - ) -> "ModelSources": - """Load model source configurations from a TOML file.""" - path = Path(path) - if not path.exists(): - return cls() - - with path.open("rb") as f: - data = tomli.load(f) + bootstrap_path: str | PathLike | None = None, + user_config_path: str | PathLike | None = None, + ) -> "ModelSourceConfig": + """ + Load model source configuration. - sources = {} - for name, config in data.get("sources", {}).items(): - sources[name] = ModelSource(**config) + Parameters + ---------- + bootstrap_path : str | PathLike | None + Path to bootstrap config file. If None, uses bundled default. + If provided, ONLY this file is loaded (no user config overlay unless specified). + user_config_path : str | PathLike | None + Path to user config file to overlay on top of bootstrap. + If None and bootstrap_path is None, attempts to load from default user config location. - return cls(sources=sources) + Returns + ------- + ModelSourceConfig + Loaded and merged configuration + """ + # Load base config + if bootstrap_path is not None: + # Explicit bootstrap path - only load this file + with Path(bootstrap_path).open("rb") as f: + cfg = tomli.load(f) + else: + # Use bundled default + with _DEFAULT_CONFIG_PATH.open("rb") as f: + cfg = tomli.load(f) + + # If no explicit bootstrap path, try to load user config overlay + if user_config_path is None: + user_config_path = get_user_config_path() + + # Overlay user config if specified or found + if user_config_path is not None: + user_path = Path(user_config_path) + if user_path.exists(): + with user_path.open("rb") as f: + user_cfg = tomli.load(f) + # Merge user config sources into base config + if "sources" in user_cfg: + if "sources" not in cfg: + cfg["sources"] = {} + cfg["sources"] = cfg["sources"] | user_cfg["sources"] + + # inject source names if not explicitly provided + for name, src in cfg.get("sources", {}).items(): + if "name" not in src: + src["name"] = name + + return cls(**cfg) @classmethod - def merge(cls, base: "ModelSources", overlay: "ModelSources") -> "ModelSources": - """Merge two source configurations. Overlay takes precedence.""" - merged = dict(base.sources) - merged.update(overlay.sources) - return cls(sources=merged) + def merge(cls, base: "ModelSourceConfig", overlay: "ModelSourceConfig") -> "ModelSourceConfig": + """ + Merge two configurations, with overlay taking precedence. + + Parameters + ---------- + base : ModelSourceConfig + Base configuration + overlay : ModelSourceConfig + Configuration to overlay on top of base + + Returns + ------- + ModelSourceConfig + Merged configuration + """ + merged_sources = base.sources.copy() + merged_sources.update(overlay.sources) + return cls(sources=merged_sources) @property - def status(self) -> dict[str, ModelSource.SyncStatus]: + def status(self) -> dict[str, ModelSourceRepo.SyncStatus]: """ Sync status for all configured model source repositories. @@ -717,7 +772,7 @@ def status(self) -> dict[str, ModelSource.SyncStatus]: else: missing.append(ref) - status[name] = ModelSource.SyncStatus( + status[name] = ModelSourceRepo.SyncStatus( repo=source.repo, configured_refs=refs, cached_refs=cached, @@ -728,10 +783,10 @@ def status(self) -> dict[str, ModelSource.SyncStatus]: def sync( self, - source: str | ModelSource | None = None, + source: str | ModelSourceRepo | None = None, force: bool = False, verbose: bool = False, - ) -> dict[str, ModelSource.SyncResult]: + ) -> dict[str, ModelSourceRepo.SyncResult]: """ Synchronize registry files from model source(s). @@ -753,7 +808,7 @@ def sync( """ if source: - if isinstance(source, ModelSource): + if isinstance(source, ModelSourceRepo): if source.name not in self.sources: raise ValueError(f"Source '{source.name}' not found in bootstrap") sources = [source] @@ -1249,7 +1304,7 @@ def _try_best_effort_sync(): try: # Try to sync default refs (don't be verbose, don't fail on errors) - config = ModelSources.load() + config = ModelSourceConfig.load() config.sync(verbose=False) except Exception: # Silently fail - user will get clear error when trying to use registry diff --git a/modflow_devtools/models/__main__.py b/modflow_devtools/models/__main__.py index e22a2c34..f5786317 100644 --- a/modflow_devtools/models/__main__.py +++ b/modflow_devtools/models/__main__.py @@ -17,7 +17,7 @@ from . import ( _DEFAULT_CACHE, - ModelSources, + ModelSourceConfig, _try_best_effort_sync, ) @@ -47,7 +47,7 @@ def _format_grid(items, prefix=""): def cmd_sync(args): """Sync command handler.""" - config = ModelSources.load() + config = ModelSourceConfig.load() # If a specific source is provided, sync just that source if args.source: @@ -65,9 +65,9 @@ def cmd_sync(args): if source_obj is None: # If --repo is provided, create an ad-hoc source if args.repo: - from . import ModelSource + from . import ModelSourceRepo - source_obj = ModelSource( + source_obj = ModelSourceRepo( repo=args.repo, name=args.source, refs=[args.ref] if args.ref else [], @@ -111,7 +111,7 @@ def cmd_info(args): if os.environ.get("MODFLOW_DEVTOOLS_AUTO_SYNC", "").lower() in ("1", "true", "yes"): _try_best_effort_sync() - config = ModelSources.load() + config = ModelSourceConfig.load() status = config.status if not status: diff --git a/modflow_devtools/programs/__init__.py b/modflow_devtools/programs/__init__.py index afd49cf1..1de99248 100644 --- a/modflow_devtools/programs/__init__.py +++ b/modflow_devtools/programs/__init__.py @@ -836,7 +836,7 @@ def download_archive( if github_token: headers["Authorization"] = f"token {github_token}" - response = requests.get(url, headers=headers, stream=True, timeout=30) # type: ignore + response = requests.get(url, headers=headers, stream=True, timeout=30) response.raise_for_status() # Write to temporary file first From 591d0f9ef06839bcd696905ee0b0a1096c7462df Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Thu, 21 May 2026 11:29:39 -0700 Subject: [PATCH 13/29] more fixes, fix tests --- autotest/autotest/temp/dfn/chf-cdb.dfn | 183 ++++ autotest/autotest/temp/dfn/chf-chd.dfn | 208 ++++ autotest/autotest/temp/dfn/chf-cxs.dfn | 100 ++ autotest/autotest/temp/dfn/chf-dfw.dfn | 143 +++ autotest/autotest/temp/dfn/chf-disv1d.dfn | 250 +++++ autotest/autotest/temp/dfn/chf-evp.dfn | 211 ++++ autotest/autotest/temp/dfn/chf-flw.dfn | 207 ++++ autotest/autotest/temp/dfn/chf-ic.dfn | 22 + autotest/autotest/temp/dfn/chf-nam.dfn | 99 ++ autotest/autotest/temp/dfn/chf-oc.dfn | 350 +++++++ autotest/autotest/temp/dfn/chf-pcp.dfn | 211 ++++ autotest/autotest/temp/dfn/chf-sto.dfn | 60 ++ autotest/autotest/temp/dfn/chf-zdg.dfn | 237 +++++ autotest/autotest/temp/dfn/common.dfn | 61 ++ autotest/autotest/temp/dfn/exg-chfgwf.dfn | 136 +++ autotest/autotest/temp/dfn/exg-gwegwe.dfn | 281 +++++ autotest/autotest/temp/dfn/exg-gwfgwe.dfn | 3 + autotest/autotest/temp/dfn/exg-gwfgwf.dfn | 321 ++++++ autotest/autotest/temp/dfn/exg-gwfgwt.dfn | 3 + autotest/autotest/temp/dfn/exg-gwfprt.dfn | 3 + autotest/autotest/temp/dfn/exg-gwtgwt.dfn | 281 +++++ autotest/autotest/temp/dfn/exg-olfgwf.dfn | 136 +++ autotest/autotest/temp/dfn/gwe-adv.dfn | 19 + autotest/autotest/temp/dfn/gwe-cnd.dfn | 118 +++ autotest/autotest/temp/dfn/gwe-ctp.dfn | 213 ++++ autotest/autotest/temp/dfn/gwe-dis.dfn | 239 +++++ autotest/autotest/temp/dfn/gwe-disu.dfn | 337 ++++++ autotest/autotest/temp/dfn/gwe-disv.dfn | 320 ++++++ autotest/autotest/temp/dfn/gwe-esl.dfn | 211 ++++ autotest/autotest/temp/dfn/gwe-est.dfn | 108 ++ autotest/autotest/temp/dfn/gwe-fmi.dfn | 59 ++ autotest/autotest/temp/dfn/gwe-ic.dfn | 33 + autotest/autotest/temp/dfn/gwe-lke.dfn | 481 +++++++++ autotest/autotest/temp/dfn/gwe-mve.dfn | 106 ++ autotest/autotest/temp/dfn/gwe-mwe.dfn | 447 ++++++++ autotest/autotest/temp/dfn/gwe-nam.dfn | 210 ++++ autotest/autotest/temp/dfn/gwe-oc.dfn | 318 ++++++ autotest/autotest/temp/dfn/gwe-sfe.dfn | 480 +++++++++ autotest/autotest/temp/dfn/gwe-ssm.dfn | 125 +++ autotest/autotest/temp/dfn/gwe-uze.dfn | 438 ++++++++ autotest/autotest/temp/dfn/gwf-api.dfn | 100 ++ autotest/autotest/temp/dfn/gwf-buy.dfn | 148 +++ autotest/autotest/temp/dfn/gwf-chd.dfn | 222 ++++ autotest/autotest/temp/dfn/gwf-chdg.dfn | 170 +++ autotest/autotest/temp/dfn/gwf-csub.dfn | 793 ++++++++++++++ autotest/autotest/temp/dfn/gwf-dis.dfn | 237 +++++ autotest/autotest/temp/dfn/gwf-disu.dfn | 337 ++++++ autotest/autotest/temp/dfn/gwf-disv.dfn | 320 ++++++ autotest/autotest/temp/dfn/gwf-drn.dfn | 251 +++++ autotest/autotest/temp/dfn/gwf-drng.dfn | 198 ++++ autotest/autotest/temp/dfn/gwf-evt.dfn | 295 ++++++ autotest/autotest/temp/dfn/gwf-evta.dfn | 220 ++++ autotest/autotest/temp/dfn/gwf-ghb.dfn | 232 +++++ autotest/autotest/temp/dfn/gwf-ghbg.dfn | 179 ++++ autotest/autotest/temp/dfn/gwf-gnc.dfn | 100 ++ autotest/autotest/temp/dfn/gwf-hfb.dfn | 77 ++ autotest/autotest/temp/dfn/gwf-ic.dfn | 33 + autotest/autotest/temp/dfn/gwf-lak.dfn | 884 ++++++++++++++++ autotest/autotest/temp/dfn/gwf-maw.dfn | 762 ++++++++++++++ autotest/autotest/temp/dfn/gwf-mvr.dfn | 265 +++++ autotest/autotest/temp/dfn/gwf-nam.dfn | 226 ++++ autotest/autotest/temp/dfn/gwf-npf.dfn | 375 +++++++ autotest/autotest/temp/dfn/gwf-oc.dfn | 317 ++++++ autotest/autotest/temp/dfn/gwf-rch.dfn | 221 ++++ autotest/autotest/temp/dfn/gwf-rcha.dfn | 201 ++++ autotest/autotest/temp/dfn/gwf-riv.dfn | 243 +++++ autotest/autotest/temp/dfn/gwf-rivg.dfn | 191 ++++ autotest/autotest/temp/dfn/gwf-sfr.dfn | 970 +++++++++++++++++ autotest/autotest/temp/dfn/gwf-sto.dfn | 187 ++++ autotest/autotest/temp/dfn/gwf-uzf.dfn | 611 +++++++++++ autotest/autotest/temp/dfn/gwf-vsc.dfn | 177 ++++ autotest/autotest/temp/dfn/gwf-wel.dfn | 285 +++++ autotest/autotest/temp/dfn/gwf-welg.dfn | 233 +++++ autotest/autotest/temp/dfn/gwt-adv.dfn | 18 + autotest/autotest/temp/dfn/gwt-api.dfn | 100 ++ autotest/autotest/temp/dfn/gwt-cnc.dfn | 213 ++++ autotest/autotest/temp/dfn/gwt-dis.dfn | 239 +++++ autotest/autotest/temp/dfn/gwt-disu.dfn | 337 ++++++ autotest/autotest/temp/dfn/gwt-disv.dfn | 320 ++++++ autotest/autotest/temp/dfn/gwt-dsp.dfn | 106 ++ autotest/autotest/temp/dfn/gwt-fmi.dfn | 59 ++ autotest/autotest/temp/dfn/gwt-ic.dfn | 33 + autotest/autotest/temp/dfn/gwt-ist.dfn | 377 +++++++ autotest/autotest/temp/dfn/gwt-lkt.dfn | 460 +++++++++ autotest/autotest/temp/dfn/gwt-mst.dfn | 168 +++ autotest/autotest/temp/dfn/gwt-mvt.dfn | 106 ++ autotest/autotest/temp/dfn/gwt-mwt.dfn | 427 ++++++++ autotest/autotest/temp/dfn/gwt-nam.dfn | 210 ++++ autotest/autotest/temp/dfn/gwt-oc.dfn | 318 ++++++ autotest/autotest/temp/dfn/gwt-sft.dfn | 460 +++++++++ autotest/autotest/temp/dfn/gwt-src.dfn | 222 ++++ autotest/autotest/temp/dfn/gwt-ssm.dfn | 125 +++ autotest/autotest/temp/dfn/gwt-uzt.dfn | 438 ++++++++ autotest/autotest/temp/dfn/olf-cdb.dfn | 183 ++++ autotest/autotest/temp/dfn/olf-chd.dfn | 208 ++++ autotest/autotest/temp/dfn/olf-cxs.dfn | 100 ++ autotest/autotest/temp/dfn/olf-dfw.dfn | 143 +++ autotest/autotest/temp/dfn/olf-dis2d.dfn | 163 +++ autotest/autotest/temp/dfn/olf-disv1d.dfn | 261 +++++ autotest/autotest/temp/dfn/olf-disv2d.dfn | 247 +++++ autotest/autotest/temp/dfn/olf-evp.dfn | 211 ++++ autotest/autotest/temp/dfn/olf-flw.dfn | 207 ++++ autotest/autotest/temp/dfn/olf-ic.dfn | 22 + autotest/autotest/temp/dfn/olf-nam.dfn | 99 ++ autotest/autotest/temp/dfn/olf-oc.dfn | 350 +++++++ autotest/autotest/temp/dfn/olf-pcp.dfn | 211 ++++ autotest/autotest/temp/dfn/olf-sto.dfn | 60 ++ autotest/autotest/temp/dfn/olf-zdg.dfn | 237 +++++ autotest/autotest/temp/dfn/prt-dis.dfn | 230 +++++ autotest/autotest/temp/dfn/prt-disv.dfn | 313 ++++++ autotest/autotest/temp/dfn/prt-fmi.dfn | 50 + autotest/autotest/temp/dfn/prt-mip.dfn | 41 + autotest/autotest/temp/dfn/prt-nam.dfn | 73 ++ autotest/autotest/temp/dfn/prt-oc.dfn | 451 ++++++++ autotest/autotest/temp/dfn/prt-prp.dfn | 508 +++++++++ autotest/autotest/temp/dfn/sim-nam.dfn | 255 +++++ autotest/autotest/temp/dfn/sim-tdis.dfn | 113 ++ autotest/autotest/temp/dfn/sln-ems.dfn | 1 + autotest/autotest/temp/dfn/sln-ims.dfn | 389 +++++++ autotest/autotest/temp/dfn/sln-pts.dfn | 178 ++++ autotest/autotest/temp/dfn/swf-cdb.dfn | 183 ++++ autotest/autotest/temp/dfn/swf-chd.dfn | 208 ++++ autotest/autotest/temp/dfn/swf-cxs.dfn | 100 ++ autotest/autotest/temp/dfn/swf-dfw.dfn | 143 +++ autotest/autotest/temp/dfn/swf-dis2d.dfn | 163 +++ autotest/autotest/temp/dfn/swf-disv1d.dfn | 250 +++++ autotest/autotest/temp/dfn/swf-disv2d.dfn | 247 +++++ autotest/autotest/temp/dfn/swf-evp.dfn | 211 ++++ autotest/autotest/temp/dfn/swf-flw.dfn | 207 ++++ autotest/autotest/temp/dfn/swf-ic.dfn | 22 + autotest/autotest/temp/dfn/swf-nam.dfn | 99 ++ autotest/autotest/temp/dfn/swf-oc.dfn | 350 +++++++ autotest/autotest/temp/dfn/swf-pcp.dfn | 211 ++++ autotest/autotest/temp/dfn/swf-sto.dfn | 60 ++ autotest/autotest/temp/dfn/swf-zdg.dfn | 237 +++++ .../autotest/temp/dfn/toml-v1_1/sim-nam.toml | 588 +++++++++++ autotest/autotest/temp/dfn/toml/chf-cdb.toml | 124 +++ autotest/autotest/temp/dfn/toml/chf-chd.toml | 146 +++ autotest/autotest/temp/dfn/toml/chf-cxs.toml | 82 ++ autotest/autotest/temp/dfn/toml/chf-dfw.toml | 98 ++ .../autotest/temp/dfn/toml/chf-disv1d.toml | 198 ++++ autotest/autotest/temp/dfn/toml/chf-evp.toml | 146 +++ autotest/autotest/temp/dfn/toml/chf-flw.toml | 146 +++ autotest/autotest/temp/dfn/toml/chf-ic.toml | 23 + autotest/autotest/temp/dfn/toml/chf-nam.toml | 78 ++ autotest/autotest/temp/dfn/toml/chf-oc.toml | 187 ++++ autotest/autotest/temp/dfn/toml/chf-pcp.toml | 146 +++ autotest/autotest/temp/dfn/toml/chf-sto.toml | 33 + autotest/autotest/temp/dfn/toml/chf-zdg.toml | 159 +++ .../autotest/temp/dfn/toml/exg-chfgwf.toml | 93 ++ .../autotest/temp/dfn/toml/exg-gwegwe.toml | 218 ++++ .../autotest/temp/dfn/toml/exg-gwfgwe.toml | 3 + .../autotest/temp/dfn/toml/exg-gwfgwf.toml | 252 +++++ .../autotest/temp/dfn/toml/exg-gwfgwt.toml | 3 + .../autotest/temp/dfn/toml/exg-gwfprt.toml | 3 + .../autotest/temp/dfn/toml/exg-gwtgwt.toml | 218 ++++ .../autotest/temp/dfn/toml/exg-olfgwf.toml | 93 ++ autotest/autotest/temp/dfn/toml/gwe-adv.toml | 41 + autotest/autotest/temp/dfn/toml/gwe-cnd.toml | 109 ++ autotest/autotest/temp/dfn/toml/gwe-ctp.toml | 146 +++ autotest/autotest/temp/dfn/toml/gwe-dis.toml | 201 ++++ autotest/autotest/temp/dfn/toml/gwe-disu.toml | 288 ++++++ autotest/autotest/temp/dfn/toml/gwe-disv.toml | 249 +++++ autotest/autotest/temp/dfn/toml/gwe-esl.toml | 146 +++ autotest/autotest/temp/dfn/toml/gwe-est.toml | 94 ++ autotest/autotest/temp/dfn/toml/gwe-fmi.toml | 46 + autotest/autotest/temp/dfn/toml/gwe-ic.toml | 30 + autotest/autotest/temp/dfn/toml/gwe-lke.toml | 290 ++++++ autotest/autotest/temp/dfn/toml/gwe-mve.toml | 69 ++ autotest/autotest/temp/dfn/toml/gwe-mwe.toml | 272 +++++ autotest/autotest/temp/dfn/toml/gwe-nam.toml | 133 +++ autotest/autotest/temp/dfn/toml/gwe-oc.toml | 166 +++ autotest/autotest/temp/dfn/toml/gwe-sfe.toml | 290 ++++++ autotest/autotest/temp/dfn/toml/gwe-ssm.toml | 84 ++ autotest/autotest/temp/dfn/toml/gwe-uze.toml | 266 +++++ autotest/autotest/temp/dfn/toml/gwf-api.toml | 68 ++ autotest/autotest/temp/dfn/toml/gwf-buy.toml | 97 ++ autotest/autotest/temp/dfn/toml/gwf-chd.toml | 152 +++ autotest/autotest/temp/dfn/toml/gwf-chdg.toml | 116 +++ autotest/autotest/temp/dfn/toml/gwf-csub.toml | 509 +++++++++ autotest/autotest/temp/dfn/toml/gwf-dis.toml | 201 ++++ autotest/autotest/temp/dfn/toml/gwf-disu.toml | 290 ++++++ autotest/autotest/temp/dfn/toml/gwf-disv.toml | 249 +++++ autotest/autotest/temp/dfn/toml/gwf-drn.toml | 171 +++ autotest/autotest/temp/dfn/toml/gwf-drng.toml | 138 +++ autotest/autotest/temp/dfn/toml/gwf-evt.toml | 212 ++++ autotest/autotest/temp/dfn/toml/gwf-evta.toml | 154 +++ autotest/autotest/temp/dfn/toml/gwf-ghb.toml | 158 +++ autotest/autotest/temp/dfn/toml/gwf-ghbg.toml | 125 +++ autotest/autotest/temp/dfn/toml/gwf-gnc.toml | 89 ++ autotest/autotest/temp/dfn/toml/gwf-hfb.toml | 58 ++ autotest/autotest/temp/dfn/toml/gwf-ic.toml | 30 + autotest/autotest/temp/dfn/toml/gwf-lak.toml | 558 ++++++++++ autotest/autotest/temp/dfn/toml/gwf-maw.toml | 465 +++++++++ autotest/autotest/temp/dfn/toml/gwf-mvr.toml | 170 +++ autotest/autotest/temp/dfn/toml/gwf-nam.toml | 144 +++ autotest/autotest/temp/dfn/toml/gwf-npf.toml | 291 ++++++ autotest/autotest/temp/dfn/toml/gwf-oc.toml | 166 +++ autotest/autotest/temp/dfn/toml/gwf-rch.toml | 152 +++ autotest/autotest/temp/dfn/toml/gwf-rcha.toml | 135 +++ autotest/autotest/temp/dfn/toml/gwf-riv.toml | 165 +++ autotest/autotest/temp/dfn/toml/gwf-rivg.toml | 135 +++ autotest/autotest/temp/dfn/toml/gwf-sfr.toml | 599 +++++++++++ autotest/autotest/temp/dfn/toml/gwf-sto.toml | 117 +++ autotest/autotest/temp/dfn/toml/gwf-uzf.toml | 400 ++++++++ autotest/autotest/temp/dfn/toml/gwf-vsc.toml | 138 +++ autotest/autotest/temp/dfn/toml/gwf-wel.toml | 185 ++++ autotest/autotest/temp/dfn/toml/gwf-welg.toml | 149 +++ autotest/autotest/temp/dfn/toml/gwt-adv.toml | 46 + autotest/autotest/temp/dfn/toml/gwt-api.toml | 68 ++ autotest/autotest/temp/dfn/toml/gwt-cnc.toml | 146 +++ autotest/autotest/temp/dfn/toml/gwt-dis.toml | 201 ++++ autotest/autotest/temp/dfn/toml/gwt-disu.toml | 290 ++++++ autotest/autotest/temp/dfn/toml/gwt-disv.toml | 249 +++++ autotest/autotest/temp/dfn/toml/gwt-dsp.toml | 98 ++ autotest/autotest/temp/dfn/toml/gwt-fmi.toml | 46 + autotest/autotest/temp/dfn/toml/gwt-ic.toml | 30 + autotest/autotest/temp/dfn/toml/gwt-ist.toml | 268 +++++ autotest/autotest/temp/dfn/toml/gwt-lkt.toml | 278 +++++ autotest/autotest/temp/dfn/toml/gwt-mst.toml | 159 +++ autotest/autotest/temp/dfn/toml/gwt-mvt.toml | 69 ++ autotest/autotest/temp/dfn/toml/gwt-mwt.toml | 260 +++++ autotest/autotest/temp/dfn/toml/gwt-nam.toml | 133 +++ autotest/autotest/temp/dfn/toml/gwt-oc.toml | 166 +++ autotest/autotest/temp/dfn/toml/gwt-sft.toml | 278 +++++ autotest/autotest/temp/dfn/toml/gwt-src.toml | 152 +++ autotest/autotest/temp/dfn/toml/gwt-ssm.toml | 84 ++ autotest/autotest/temp/dfn/toml/gwt-uzt.toml | 266 +++++ autotest/autotest/temp/dfn/toml/olf-cdb.toml | 124 +++ autotest/autotest/temp/dfn/toml/olf-chd.toml | 146 +++ autotest/autotest/temp/dfn/toml/olf-cxs.toml | 82 ++ autotest/autotest/temp/dfn/toml/olf-dfw.toml | 98 ++ .../autotest/temp/dfn/toml/olf-dis2d.toml | 139 +++ .../autotest/temp/dfn/toml/olf-disv1d.toml | 207 ++++ .../autotest/temp/dfn/toml/olf-disv2d.toml | 194 ++++ autotest/autotest/temp/dfn/toml/olf-evp.toml | 146 +++ autotest/autotest/temp/dfn/toml/olf-flw.toml | 146 +++ autotest/autotest/temp/dfn/toml/olf-ic.toml | 23 + autotest/autotest/temp/dfn/toml/olf-nam.toml | 78 ++ autotest/autotest/temp/dfn/toml/olf-oc.toml | 187 ++++ autotest/autotest/temp/dfn/toml/olf-pcp.toml | 146 +++ autotest/autotest/temp/dfn/toml/olf-sto.toml | 33 + autotest/autotest/temp/dfn/toml/olf-zdg.toml | 159 +++ autotest/autotest/temp/dfn/toml/prt-dis.toml | 196 ++++ autotest/autotest/temp/dfn/toml/prt-disv.toml | 246 +++++ autotest/autotest/temp/dfn/toml/prt-fmi.toml | 40 + autotest/autotest/temp/dfn/toml/prt-mip.toml | 43 + autotest/autotest/temp/dfn/toml/prt-nam.toml | 60 ++ autotest/autotest/temp/dfn/toml/prt-oc.toml | 286 ++++++ autotest/autotest/temp/dfn/toml/prt-prp.toml | 341 ++++++ autotest/autotest/temp/dfn/toml/sim-nam.toml | 193 ++++ autotest/autotest/temp/dfn/toml/sim-tdis.toml | 82 ++ autotest/autotest/temp/dfn/toml/sln-ems.toml | 3 + autotest/autotest/temp/dfn/toml/sln-ims.toml | 285 +++++ autotest/autotest/temp/dfn/toml/sln-pts.toml | 116 +++ autotest/autotest/temp/dfn/toml/swf-cdb.toml | 124 +++ autotest/autotest/temp/dfn/toml/swf-chd.toml | 146 +++ autotest/autotest/temp/dfn/toml/swf-cxs.toml | 82 ++ autotest/autotest/temp/dfn/toml/swf-dfw.toml | 98 ++ .../autotest/temp/dfn/toml/swf-dis2d.toml | 139 +++ .../autotest/temp/dfn/toml/swf-disv1d.toml | 198 ++++ .../autotest/temp/dfn/toml/swf-disv2d.toml | 194 ++++ autotest/autotest/temp/dfn/toml/swf-evp.toml | 146 +++ autotest/autotest/temp/dfn/toml/swf-flw.toml | 146 +++ autotest/autotest/temp/dfn/toml/swf-ic.toml | 23 + autotest/autotest/temp/dfn/toml/swf-nam.toml | 78 ++ autotest/autotest/temp/dfn/toml/swf-oc.toml | 187 ++++ autotest/autotest/temp/dfn/toml/swf-pcp.toml | 146 +++ autotest/autotest/temp/dfn/toml/swf-sto.toml | 33 + autotest/autotest/temp/dfn/toml/swf-zdg.toml | 159 +++ autotest/autotest/temp/dfn/toml/utl-ats.toml | 63 ++ autotest/autotest/temp/dfn/toml/utl-hpc.toml | 44 + .../autotest/temp/dfn/toml/utl-laktab.toml | 61 ++ autotest/autotest/temp/dfn/toml/utl-ncf.toml | 103 ++ autotest/autotest/temp/dfn/toml/utl-obs.toml | 57 + .../autotest/temp/dfn/toml/utl-sfrtab.toml | 55 + autotest/autotest/temp/dfn/toml/utl-spc.toml | 78 ++ autotest/autotest/temp/dfn/toml/utl-spca.toml | 64 ++ autotest/autotest/temp/dfn/toml/utl-tas.toml | 91 ++ autotest/autotest/temp/dfn/toml/utl-ts.toml | 132 +++ autotest/autotest/temp/dfn/toml/utl-tvk.toml | 74 ++ autotest/autotest/temp/dfn/toml/utl-tvs.toml | 74 ++ autotest/autotest/temp/dfn/utl-ats.dfn | 85 ++ autotest/autotest/temp/dfn/utl-hpc.dfn | 48 + autotest/autotest/temp/dfn/utl-laktab.dfn | 70 ++ autotest/autotest/temp/dfn/utl-ncf.dfn | 108 ++ autotest/autotest/temp/dfn/utl-obs.dfn | 118 +++ autotest/autotest/temp/dfn/utl-sfrtab.dfn | 60 ++ autotest/autotest/temp/dfn/utl-spc.dfn | 129 +++ autotest/autotest/temp/dfn/utl-spca.dfn | 100 ++ autotest/autotest/temp/dfn/utl-tas.dfn | 125 +++ autotest/autotest/temp/dfn/utl-ts.dfn | 184 ++++ autotest/autotest/temp/dfn/utl-tvk.dfn | 131 +++ autotest/autotest/temp/dfn/utl-tvs.dfn | 129 +++ autotest/dfn/__init__.py | 0 autotest/dfn/test_dfn.py | 414 +------- autotest/dfn/test_mapper.py | 168 ++- autotest/dfns/__init__.py | 0 autotest/dfns/test_dfns.py | 109 +- autotest/dfns/test_dfns_registry.py | 172 +++- autotest/dfns/test_dfns_schema.py | 529 ++++++---- autotest/dfns/test_mapper.py | 215 +++- autotest/test_models.py | 77 +- docs/md/dev/dfns.md | 971 ++---------------- docs/md/dfns.md | 289 +++--- modflow_devtools/dfn/__init__.py | 3 +- modflow_devtools/dfn/mapper.py | 28 +- modflow_devtools/dfn/schema.py | 88 +- modflow_devtools/dfn2toml.py | 4 +- modflow_devtools/dfns/__main__.py | 15 +- modflow_devtools/dfns/dfns.toml | 3 +- modflow_devtools/dfns/mapper.py | 212 ++-- modflow_devtools/dfns/registry.py | 55 +- modflow_devtools/dfns/schema.py | 196 ++-- modflow_devtools/imports.py | 2 - 315 files changed, 55860 insertions(+), 2160 deletions(-) create mode 100644 autotest/autotest/temp/dfn/chf-cdb.dfn create mode 100644 autotest/autotest/temp/dfn/chf-chd.dfn create mode 100644 autotest/autotest/temp/dfn/chf-cxs.dfn create mode 100644 autotest/autotest/temp/dfn/chf-dfw.dfn create mode 100644 autotest/autotest/temp/dfn/chf-disv1d.dfn create mode 100644 autotest/autotest/temp/dfn/chf-evp.dfn create mode 100644 autotest/autotest/temp/dfn/chf-flw.dfn create mode 100644 autotest/autotest/temp/dfn/chf-ic.dfn create mode 100644 autotest/autotest/temp/dfn/chf-nam.dfn create mode 100644 autotest/autotest/temp/dfn/chf-oc.dfn create mode 100644 autotest/autotest/temp/dfn/chf-pcp.dfn create mode 100644 autotest/autotest/temp/dfn/chf-sto.dfn create mode 100644 autotest/autotest/temp/dfn/chf-zdg.dfn create mode 100644 autotest/autotest/temp/dfn/common.dfn create mode 100644 autotest/autotest/temp/dfn/exg-chfgwf.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwegwe.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwfgwe.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwfgwf.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwfgwt.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwfprt.dfn create mode 100644 autotest/autotest/temp/dfn/exg-gwtgwt.dfn create mode 100644 autotest/autotest/temp/dfn/exg-olfgwf.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-adv.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-cnd.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-ctp.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-dis.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-disu.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-disv.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-esl.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-est.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-fmi.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-ic.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-lke.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-mve.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-mwe.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-nam.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-oc.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-sfe.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-ssm.dfn create mode 100644 autotest/autotest/temp/dfn/gwe-uze.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-api.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-buy.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-chd.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-chdg.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-csub.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-dis.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-disu.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-disv.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-drn.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-drng.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-evt.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-evta.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-ghb.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-ghbg.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-gnc.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-hfb.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-ic.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-lak.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-maw.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-mvr.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-nam.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-npf.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-oc.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-rch.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-rcha.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-riv.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-rivg.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-sfr.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-sto.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-uzf.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-vsc.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-wel.dfn create mode 100644 autotest/autotest/temp/dfn/gwf-welg.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-adv.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-api.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-cnc.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-dis.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-disu.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-disv.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-dsp.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-fmi.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-ic.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-ist.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-lkt.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-mst.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-mvt.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-mwt.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-nam.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-oc.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-sft.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-src.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-ssm.dfn create mode 100644 autotest/autotest/temp/dfn/gwt-uzt.dfn create mode 100644 autotest/autotest/temp/dfn/olf-cdb.dfn create mode 100644 autotest/autotest/temp/dfn/olf-chd.dfn create mode 100644 autotest/autotest/temp/dfn/olf-cxs.dfn create mode 100644 autotest/autotest/temp/dfn/olf-dfw.dfn create mode 100644 autotest/autotest/temp/dfn/olf-dis2d.dfn create mode 100644 autotest/autotest/temp/dfn/olf-disv1d.dfn create mode 100644 autotest/autotest/temp/dfn/olf-disv2d.dfn create mode 100644 autotest/autotest/temp/dfn/olf-evp.dfn create mode 100644 autotest/autotest/temp/dfn/olf-flw.dfn create mode 100644 autotest/autotest/temp/dfn/olf-ic.dfn create mode 100644 autotest/autotest/temp/dfn/olf-nam.dfn create mode 100644 autotest/autotest/temp/dfn/olf-oc.dfn create mode 100644 autotest/autotest/temp/dfn/olf-pcp.dfn create mode 100644 autotest/autotest/temp/dfn/olf-sto.dfn create mode 100644 autotest/autotest/temp/dfn/olf-zdg.dfn create mode 100644 autotest/autotest/temp/dfn/prt-dis.dfn create mode 100644 autotest/autotest/temp/dfn/prt-disv.dfn create mode 100644 autotest/autotest/temp/dfn/prt-fmi.dfn create mode 100644 autotest/autotest/temp/dfn/prt-mip.dfn create mode 100644 autotest/autotest/temp/dfn/prt-nam.dfn create mode 100644 autotest/autotest/temp/dfn/prt-oc.dfn create mode 100644 autotest/autotest/temp/dfn/prt-prp.dfn create mode 100644 autotest/autotest/temp/dfn/sim-nam.dfn create mode 100644 autotest/autotest/temp/dfn/sim-tdis.dfn create mode 100644 autotest/autotest/temp/dfn/sln-ems.dfn create mode 100644 autotest/autotest/temp/dfn/sln-ims.dfn create mode 100644 autotest/autotest/temp/dfn/sln-pts.dfn create mode 100644 autotest/autotest/temp/dfn/swf-cdb.dfn create mode 100644 autotest/autotest/temp/dfn/swf-chd.dfn create mode 100644 autotest/autotest/temp/dfn/swf-cxs.dfn create mode 100644 autotest/autotest/temp/dfn/swf-dfw.dfn create mode 100644 autotest/autotest/temp/dfn/swf-dis2d.dfn create mode 100644 autotest/autotest/temp/dfn/swf-disv1d.dfn create mode 100644 autotest/autotest/temp/dfn/swf-disv2d.dfn create mode 100644 autotest/autotest/temp/dfn/swf-evp.dfn create mode 100644 autotest/autotest/temp/dfn/swf-flw.dfn create mode 100644 autotest/autotest/temp/dfn/swf-ic.dfn create mode 100644 autotest/autotest/temp/dfn/swf-nam.dfn create mode 100644 autotest/autotest/temp/dfn/swf-oc.dfn create mode 100644 autotest/autotest/temp/dfn/swf-pcp.dfn create mode 100644 autotest/autotest/temp/dfn/swf-sto.dfn create mode 100644 autotest/autotest/temp/dfn/swf-zdg.dfn create mode 100644 autotest/autotest/temp/dfn/toml-v1_1/sim-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-cdb.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-chd.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-cxs.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-dfw.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-disv1d.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-evp.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-flw.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-pcp.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-sto.toml create mode 100644 autotest/autotest/temp/dfn/toml/chf-zdg.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-chfgwf.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwegwe.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwe.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwf.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwt.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfprt.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-gwtgwt.toml create mode 100644 autotest/autotest/temp/dfn/toml/exg-olfgwf.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-adv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-cnd.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-ctp.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-dis.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-disu.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-disv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-esl.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-est.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-fmi.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-lke.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-mve.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-mwe.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-sfe.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-ssm.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwe-uze.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-api.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-buy.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-chd.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-chdg.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-csub.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-dis.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-disu.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-disv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-drn.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-drng.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-evt.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-evta.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-ghb.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-ghbg.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-gnc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-hfb.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-lak.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-maw.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-mvr.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-npf.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-rch.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-rcha.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-riv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-rivg.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-sfr.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-sto.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-uzf.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-vsc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-wel.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwf-welg.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-adv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-api.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-cnc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-dis.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-disu.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-disv.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-dsp.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-fmi.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-ist.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-lkt.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-mst.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-mvt.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-mwt.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-sft.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-src.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-ssm.toml create mode 100644 autotest/autotest/temp/dfn/toml/gwt-uzt.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-cdb.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-chd.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-cxs.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-dfw.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-dis2d.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-disv1d.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-disv2d.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-evp.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-flw.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-pcp.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-sto.toml create mode 100644 autotest/autotest/temp/dfn/toml/olf-zdg.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-dis.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-disv.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-fmi.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-mip.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/prt-prp.toml create mode 100644 autotest/autotest/temp/dfn/toml/sim-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/sim-tdis.toml create mode 100644 autotest/autotest/temp/dfn/toml/sln-ems.toml create mode 100644 autotest/autotest/temp/dfn/toml/sln-ims.toml create mode 100644 autotest/autotest/temp/dfn/toml/sln-pts.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-cdb.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-chd.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-cxs.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-dfw.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-dis2d.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-disv1d.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-disv2d.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-evp.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-flw.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-ic.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-nam.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-oc.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-pcp.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-sto.toml create mode 100644 autotest/autotest/temp/dfn/toml/swf-zdg.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-ats.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-hpc.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-laktab.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-ncf.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-obs.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-sfrtab.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-spc.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-spca.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-tas.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-ts.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-tvk.toml create mode 100644 autotest/autotest/temp/dfn/toml/utl-tvs.toml create mode 100644 autotest/autotest/temp/dfn/utl-ats.dfn create mode 100644 autotest/autotest/temp/dfn/utl-hpc.dfn create mode 100644 autotest/autotest/temp/dfn/utl-laktab.dfn create mode 100644 autotest/autotest/temp/dfn/utl-ncf.dfn create mode 100644 autotest/autotest/temp/dfn/utl-obs.dfn create mode 100644 autotest/autotest/temp/dfn/utl-sfrtab.dfn create mode 100644 autotest/autotest/temp/dfn/utl-spc.dfn create mode 100644 autotest/autotest/temp/dfn/utl-spca.dfn create mode 100644 autotest/autotest/temp/dfn/utl-tas.dfn create mode 100644 autotest/autotest/temp/dfn/utl-ts.dfn create mode 100644 autotest/autotest/temp/dfn/utl-tvk.dfn create mode 100644 autotest/autotest/temp/dfn/utl-tvs.dfn create mode 100644 autotest/dfn/__init__.py create mode 100644 autotest/dfns/__init__.py diff --git a/autotest/autotest/temp/dfn/chf-cdb.dfn b/autotest/autotest/temp/dfn/chf-cdb.dfn new file mode 100644 index 00000000..7cfbd98f --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-cdb.dfn @@ -0,0 +1,183 @@ +# --------------------- chf cdb options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Surface Water Flow'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'critical depth boundary'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'critical depth boundary'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'critical depth boundary'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save flows to budget file +description REPLACE save_flows {'{#1}': 'critical depth boundary'} +mf6internal ipakcb + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'CDB', '{#2}': '\\ref{table:gwf-obstypetable}'} + + +# --------------------- chf cdb dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of critical depth boundaries +description REPLACE maxbound {'{#1}': 'critical depth boundary'} + + +# --------------------- chf cdb period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid idcxs width aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name idcxs +type integer +shape +tagged false +in_record true +reader urword +time_series false +longname cross section identifier +description is the identifier for the cross section specified in the CXS Package. A value of zero indicates the zero-depth-gradient calculation will use parameters for a hydraulically wide channel. +numeric_index true + +block period +name width +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname width of the zero-depth gradient boundary +description is the channel width of the zero-depth gradient boundary. If a cross section is associated with this boundary, the width will be scaled by the cross section information. 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'zero-depth-gradient boundary'} +mf6internal auxvar + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname zero-depth-gradient boundary name +description REPLACE boundname {'{#1}': 'zero-depth-gradient boundary'} diff --git a/autotest/autotest/temp/dfn/chf-chd.dfn b/autotest/autotest/temp/dfn/chf-chd.dfn new file mode 100644 index 00000000..a178a821 --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-chd.dfn @@ -0,0 +1,208 @@ +# --------------------- chf chd options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Surface Water Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'CHD head value'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'constant-head'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'constant-head'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print CHD flows to listing file +description REPLACE print_flows {'{#1}': 'constant-head'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save CHD flows to budget file +description REPLACE save_flows {'{#1}': 'constant-head'} + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'CHD', '{#2}': '\\ref{table:gwf-obstypetable}'} + + +# --------------------- chf chd dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of constant heads +description REPLACE maxbound {'{#1}': 'constant-head'} + + +# --------------------- chf chd period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid head aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name head +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname head value assigned to constant head +description is the head at the boundary. 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'constant head'} + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname constant head boundary name +description REPLACE boundname {'{#1}': 'constant head boundary'} diff --git a/autotest/autotest/temp/dfn/chf-cxs.dfn b/autotest/autotest/temp/dfn/chf-cxs.dfn new file mode 100644 index 00000000..0fe273aa --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-cxs.dfn @@ -0,0 +1,100 @@ +# --------------------- chf cxs options --------------------- + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'stream reach'} +mf6internal iprpak + +# --------------------- chf cxs dimensions --------------------- + +block dimensions +name nsections +type integer +reader urword +optional false +longname number of reaches +description integer value specifying the number of cross sections that will be defined. There must be NSECTIONS entries in the PACKAGEDATA block. + +block dimensions +name npoints +type integer +reader urword +optional false +longname total number of points defined for all reaches +description integer value specifying the total number of cross-section points defined for all reaches. There must be NPOINTS entries in the CROSSSECTIONDATA block. + +# --------------------- chf cxs packagedata --------------------- + +block packagedata +name packagedata +type recarray idcxs nxspoints +shape (nsections) +reader urword +longname +description + +block packagedata +name idcxs +type integer +shape +tagged false +in_record true +reader urword +longname reach number for this entry +description integer value that defines the cross section number associated with the specified PACKAGEDATA data on the line. IDCXS must be greater than zero and less than or equal to NSECTIONS. Information must be specified for every section or the program will terminate with an error. The program will also terminate with an error if information for a section is specified more than once. +numeric_index true + +block packagedata +name nxspoints +type integer +shape +tagged false +in_record true +reader urword +longname number of points used to define cross section +description integer value that defines the number of points used to define the define the shape of a section. NXSPOINTS must be greater than 1 or the program will terminate with an error. NXSPOINTS defines the number of points that must be entered for the reach in the CROSSSECTIONDATA block. The sum of NXSPOINTS for all sections must equal the NPOINTS dimension. + +# --------------------- chf cxs crosssectiondata --------------------- + +block crosssectiondata +name crosssectiondata +type recarray xfraction height manfraction +shape (npoints) +reader urword +longname +description + +block crosssectiondata +name xfraction +type double precision +shape +tagged false +in_record true +reader urword +longname fractional width +description real value that defines the station (x) data for the cross-section as a fraction of the width (WIDTH) of the reach. XFRACTION must be greater than or equal to zero but can be greater than one. XFRACTION values can be used to decrease or increase the width of a reach from the specified reach width (WIDTH). + +block crosssectiondata +name height +type double precision +shape +tagged false +in_record true +reader urword +longname depth +description real value that is the height relative to the top of the lowest elevation of the streambed (ELEVATION) and corresponding to the station data on the same line. HEIGHT must be greater than or equal to zero and at least one cross-section height must be equal to zero. + +block crosssectiondata +name manfraction +type double precision +shape +tagged false +in_record true +reader urword +optional false +longname Manning's roughness coefficient +description real value that defines the Manning's roughness coefficient data for the cross-section as a fraction of the Manning's roughness coefficient for the reach (MANNINGSN) and corresponding to the station data on the same line. MANFRACTION must be greater than zero. MANFRACTION is applied from the XFRACTION value on the same line to the XFRACTION value on the next line. diff --git a/autotest/autotest/temp/dfn/chf-dfw.dfn b/autotest/autotest/temp/dfn/chf-dfw.dfn new file mode 100644 index 00000000..6a16ccb5 --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-dfw.dfn @@ -0,0 +1,143 @@ +# --------------------- chf dfw options --------------------- + +block options +name central_in_space +type keyword +reader urword +optional true +longname use central in space weighting +description keyword to indicate conductance should be calculated using central-in-space weighting instead of the default upstream weighting approach. This option should be used with caution as it does not work well unless all of the stream reaches are saturated. With this option, there is no way for water to flow into a dry reach from connected reaches. +mf6internal icentral + +block options +name length_conversion +type double precision +reader urword +optional true +longname length conversion factor +description real value that is used to convert user-specified Manning's roughness coefficients from meters to model length units. LENGTH\_CONVERSION should be set to 3.28081, 1.0, and 100.0 when using length units (LENGTH\_UNITS) of feet, meters, or centimeters in the simulation, respectively. LENGTH\_CONVERSION does not need to be specified if LENGTH\_UNITS are meters. +mf6internal lengthconv + +block options +name time_conversion +type double precision +reader urword +optional true +longname time conversion factor +description real value that is used to convert user-specified Manning's roughness coefficients from seconds to model time units. TIME\_CONVERSION should be set to 1.0, 60.0, 3,600.0, 86,400.0, and 31,557,600.0 when using time units (TIME\_UNITS) of seconds, minutes, hours, days, or years in the simulation, respectively. TIME\_CONVERSION does not need to be specified if TIME\_UNITS are seconds. +mf6internal timeconv + +block options +name save_flows +type keyword +reader urword +optional true +longname keyword to save DFW flows +description keyword to indicate that budget flow terms will be written to the file specified with ``BUDGET SAVE FILE'' in Output Control. +mf6internal ipakcb + +block options +name print_flows +type keyword +reader urword +optional true +longname keyword to print DFW flows to listing file +description keyword to indicate that calculated flows between cells will be printed to the listing file for every stress period time step in which ``BUDGET PRINT'' is specified in Output Control. If there is no Output Control option and ``PRINT\_FLOWS'' is specified, then flow rates are printed for the last time step of each stress period. This option can produce extremely large list files because all cell-by-cell flows are printed. It should only be used with the DFW Package for models that have a small number of cells. +mf6internal iprflow + +block options +name save_velocity +type keyword +reader urword +optional true +longname keyword to save velocity +description keyword to indicate that x, y, and z components of velocity will be calculated at cell centers and written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. If this option is activated, then additional information may be required in the discretization packages and the GWF Exchange package (if GWF models are coupled). Specifically, ANGLDEGX must be specified in the CONNECTIONDATA block of the DISU Package; ANGLDEGX must also be specified for the GWF Exchange as an auxiliary variable. +mf6internal isavvelocity + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'DFW', '{#2}': '\\ref{table:gwf-obstypetable}'} + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +# dev options + +block options +name dev_swr_conductance +type keyword +reader urword +optional true +longname use SWR conductance formulation +description use the conductance formulation in the Surface Water Routing (SWR) Process for MODFLOW-2005. +mf6internal iswrcond + +# --------------------- chf dfw griddata --------------------- + +block griddata +name manningsn +type double precision +shape (nodes) +valid +reader readarray +layered false +optional +longname mannings roughness coefficient +description mannings roughness coefficient + +block griddata +name idcxs +type integer +shape (nodes) +valid +reader readarray +layered false +optional true +longname cross section number +description integer value indication the cross section identifier in the Cross Section Package that applies to the reach. If not provided then reach will be treated as hydraulically wide. +numeric_index true diff --git a/autotest/autotest/temp/dfn/chf-disv1d.dfn b/autotest/autotest/temp/dfn/chf-disv1d.dfn new file mode 100644 index 00000000..7c4ff0b4 --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-disv1d.dfn @@ -0,0 +1,250 @@ +# --------------------- chf disv1d options --------------------- + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position origin of the model grid coordinate system +description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position origin of the model grid coordinate system +description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +# --------------------- chf disv1d dimensions --------------------- + +block dimensions +name nodes +type integer +reader urword +optional false +longname number of linear features +description is the number of linear cells. + +block dimensions +name nvert +type integer +reader urword +optional true +longname number of columns +description is the total number of (x, y, z) vertex pairs used to characterize the model grid. + +# --------------------- chf disv1d griddata --------------------- + +block griddata +name width +type double precision +shape (nodes) +valid +reader readarray +layered false +optional +longname width +description real value that defines the width for each one-dimensional cell. WIDTH must be greater than zero. If the Cross Section (CXS) Package is used, then the WIDTH value will be multiplied by the specified cross section x-fraction values. + +block griddata +name bottom +type double precision +shape (nodes) +valid +reader readarray +layered false +optional +longname bottom elevation for the one-dimensional cell +description bottom elevation for the one-dimensional cell. + +block griddata +name idomain +type integer +shape (nodes) +reader readarray +layered false +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. + + +# --------------------- chf disv1d vertices --------------------- + +block vertices +name vertices +type recarray iv xv yv +shape (nvert) +reader urword +optional false +longname vertices data +description + +block vertices +name iv +type integer +in_record true +tagged false +reader urword +optional false +longname vertex number +description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. +numeric_index true + +block vertices +name xv +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for vertex +description is the x-coordinate for the vertex. + +block vertices +name yv +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for vertex +description is the y-coordinate for the vertex. + +# --------------------- chf disv1d cell1d --------------------- + +block cell1d +name cell1d +type recarray icell1d fdc ncvert icvert +shape (nodes) +reader urword +optional false +longname cell1d data +description + +block cell1d +name icell1d +type integer +in_record true +tagged false +reader urword +optional false +longname cell1d number +description is the cell1d number. Records in the cell1d block must be listed in consecutive order from the first to the last. +numeric_index true + +block cell1d +name fdc +type double precision +in_record true +tagged false +reader urword +optional false +longname fractional distance to the cell center +description is the fractional distance to the cell center. FDC is relative to the first vertex in the ICVERT array. In most cases FDC should be 0.5, which would place the center of the line segment that defines the cell. If the value of FDC is 1, the cell center would located at the last vertex. FDC values of 0 and 1 can be used to place the node at either end of the cell which can be useful for cells with boundary conditions. + +block cell1d +name ncvert +type integer +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. + +block cell1d +name icvert +type integer +shape (ncvert) +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in the order that defines the line representing the cell. Cells that are connected must share vertices. The bottom elevation of the cell is calculated using the ZV of the first and last vertex point and FDC. +numeric_index true diff --git a/autotest/autotest/temp/dfn/chf-evp.dfn b/autotest/autotest/temp/dfn/chf-evp.dfn new file mode 100644 index 00000000..aadeac9b --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-evp.dfn @@ -0,0 +1,211 @@ +# --------------------- swf evp options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Surface Water Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'evaporation'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'evaporation'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'evaporation'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print evaporation rates to listing file +description REPLACE print_flows {'{#1}': 'evaporation'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save evaporation to budget file +description REPLACE save_flows {'{#1}': 'evaporation'} +mf6internal ipakcb + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'EVP', '{#2}': '\\ref{table:gwf-obstypetable}'} + +# --------------------- swf evp dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of evaporation cells +description REPLACE maxbound {'{#1}': 'evaporation'} + + +# --------------------- swf evp period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid evaporation aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name evaporation +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname evaporation rate +description is the evaporation flux rate ($LT^{-1}$). This rate is multiplied inside the program by the water surface area of the cell to calculate the volumetric evaporation rate. 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'evaporation'} +mf6internal auxvar + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname evaporation name +description REPLACE boundname {'{#1}': 'evaporation'} diff --git a/autotest/autotest/temp/dfn/chf-flw.dfn b/autotest/autotest/temp/dfn/chf-flw.dfn new file mode 100644 index 00000000..09cef641 --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-flw.dfn @@ -0,0 +1,207 @@ +# --------------------- chf flw options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Stream Network Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'flow rate'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'inflow'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'inflow'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'inflow'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'inflow'} + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'FLW', '{#2}': '\\ref{table:gwf-obstypetable}'} + +# --------------------- chf flw dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of inflow +description REPLACE maxbound {'{#1}': 'inflow'} + + +# --------------------- chf flw period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid q aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name q +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname well rate +description is the volumetric inflow rate. A positive value indicates inflow to the stream. Negative values are not allows. 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'inflow'} + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname inflow name +description REPLACE boundname {'{#1}': 'inflow'} diff --git a/autotest/autotest/temp/dfn/chf-ic.dfn b/autotest/autotest/temp/dfn/chf-ic.dfn new file mode 100644 index 00000000..a483e8db --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-ic.dfn @@ -0,0 +1,22 @@ +# --------------------- chf ic options --------------------- + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +# --------------------- chf ic griddata --------------------- + +block griddata +name strt +type double precision +shape (nodes) +reader readarray +layered true +longname starting concentration +description is the initial (starting) stage---that is, stage at the beginning of the CHF Model simulation. STRT must be specified for all CHF Model simulations. One value is read for every model reach. +default_value 0.0 diff --git a/autotest/autotest/temp/dfn/chf-nam.dfn b/autotest/autotest/temp/dfn/chf-nam.dfn new file mode 100644 index 00000000..dd2b16d3 --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-nam.dfn @@ -0,0 +1,99 @@ +# --------------------- chf nam options --------------------- + +block options +name list +type string +reader urword +optional true +preserve_case true +longname name of listing file +description is name of the listing file to create for this CHF model. If not specified, then the name of the list file will be the basename of the CHF model name file and the '.lst' extension. For example, if the CHF name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'all model stress package'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'all model package'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save flows for all packages to budget file +description REPLACE save_flows {'{#1}': 'all model package'} + +block options +name newtonoptions +type record newton under_relaxation +reader urword +optional true +longname newton keyword and options +description none + +block options +name newton +in_record true +type keyword +reader urword +longname keyword to activate Newton-Raphson formulation +description keyword that activates the Newton-Raphson formulation for surface water flow between connected reaches and stress packages that support calculation of Newton-Raphson terms. + +block options +name under_relaxation +in_record true +type keyword +reader urword +optional true +longname keyword to activate Newton-Raphson UNDER_RELAXATION option +description keyword that indicates whether the surface water stage in a reach will be under-relaxed when water levels fall below the bottom of the model below any given cell. By default, Newton-Raphson UNDER\_RELAXATION is not applied. + +# --------------------- chf nam packages --------------------- + +block packages +name packages +type recarray ftype fname pname +reader urword +optional false +longname package list +description + +block packages +name ftype +in_record true +type string +tagged false +reader urword +longname package type +description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-chf}. Ftype may be entered in any combination of uppercase and lowercase. + +block packages +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. + +block packages +name pname +in_record true +type string +tagged false +reader urword +optional true +longname user name for package +description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single CHF Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. + diff --git a/autotest/autotest/temp/dfn/chf-oc.dfn b/autotest/autotest/temp/dfn/chf-oc.dfn new file mode 100644 index 00000000..6df35aec --- /dev/null +++ b/autotest/autotest/temp/dfn/chf-oc.dfn @@ -0,0 +1,350 @@ +# --------------------- chf oc options --------------------- + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +mf6internal budfilerec +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +mf6internal budcsvfilerec +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name qoutflow_filerecord +type record qoutflow fileout qoutflowfile +shape +reader urword +tagged true +optional true +mf6internal qoutfilerec +longname +description + +block options +name qoutflow +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname qoutflow keyword +description keyword to specify that record corresponds to qoutflow. + +block options +name qoutflowfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write conc information. + +block options +name stage_filerecord +type record stage fileout stagefile +shape +reader urword +tagged true +optional true +mf6internal stagefilerec +longname +description + +block options +name stage +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname stage keyword +description keyword to specify that record corresponds to stage. + +block options +name stagefile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write stage information. + +block options +name qoutflowprintrecord +type record qoutflow print_format formatrecord +shape +reader urword +optional true +mf6internal qoutprintrec +longname +description + +block options +name print_format +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to indicate that a print format follows +description keyword to specify format for printing to the listing file. + +block options +name formatrecord +type record columns width digits format +shape +in_record true +reader urword +tagged +optional false +longname +description + +block options +name columns +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of columns +description number of columns for writing data. + +block options +name width +type integer +shape +in_record true +reader urword +tagged true +optional +longname width for each number +description width for writing each number. + +block options +name digits +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of digits +description number of digits to use for writing a number. + +block options +name format +type string +shape +in_record true +reader urword +tagged false +optional false +longname write format +description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. + +# --------------------- chf oc period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name saverecord +type record save rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name save +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be saved this stress period. + +block period +name printrecord +type record print rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name print +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be printed this stress period. + +block period +name rtype +type string +shape +in_record true +reader urword +tagged false +optional false +longname record type +description type of information to save or print. Can be BUDGET. + +block period +name ocsetting +type keystring all first last frequency steps +shape +tagged false +in_record true +reader urword +longname +description specifies the steps for which the data will be saved. + +block period +name all +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for all time steps in period. + +block period +name first +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name last +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name frequency +type integer +shape +tagged true +in_record true +reader urword +longname +description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name steps +type integer +shape ($ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. + +block exchangedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +longname auxiliary variables +description represents the values of the auxiliary variables for each GWEGWE Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. +mf6internal auxvar + +block exchangedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname exchange boundname +description REPLACE boundname {'{#1}': 'GWE Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-gwfgwe.dfn b/autotest/autotest/temp/dfn/exg-gwfgwe.dfn new file mode 100644 index 00000000..fe541026 --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-gwfgwe.dfn @@ -0,0 +1,3 @@ +# --------------------- exg gwfgwe options --------------------- + + diff --git a/autotest/autotest/temp/dfn/exg-gwfgwf.dfn b/autotest/autotest/temp/dfn/exg-gwfgwf.dfn new file mode 100644 index 00000000..0f68acea --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-gwfgwf.dfn @@ -0,0 +1,321 @@ +# --------------------- exg gwfgwf options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description an array of auxiliary variable names. There is no limit on the number of auxiliary variables that can be provided. Most auxiliary variables will not be used by the GWF-GWF Exchange, but they will be available for use by other parts of the program. If an auxiliary variable with the name ``ANGLDEGX'' is found, then this information will be used as the angle (provided in degrees) between the connection face normal and the x axis, where a value of zero indicates that a normal vector points directly along the positive x axis. The connection face normal is a normal vector on the cell face shared between the cell in model 1 and the cell in model 2 pointing away from the model 1 cell. Additional information on ``ANGLDEGX'' and when it is required is provided in the description of the DISU Package. If an auxiliary variable with the name ``CDIST'' is found, then this information will be used in the calculation of specific discharge within model cells connected by the exchange. For a horizontal connection, CDIST should be specified as the horizontal distance between the cell centers, and should not include the vertical component. For vertical connections, CDIST should be specified as the difference in elevation between the two cell centers. Both ANGLDEGX and CDIST are required if specific discharge is calculated for either of the groundwater models. + + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'GWF Exchange'} + +block options +name print_input +type keyword +reader urword +optional true +longname keyword to print input to list file +description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname keyword to print gwfgwf flows to list file +description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname keyword to save GWFGWF flows +description keyword to indicate that cell-by-cell flow terms will be written to the budget file for each model provided that the Output Control for the models are set up with the ``BUDGET SAVE FILE'' option. +mf6internal ipakcb + +block options +name cell_averaging +type string +valid harmonic logarithmic amt-lmk +reader urword +optional true +longname conductance weighting option +description is a keyword and text keyword to indicate the method that will be used for calculating the conductance for horizontal cell connections. The text value for CELL\_AVERAGING can be ``HARMONIC'', ``LOGARITHMIC'', or ``AMT-LMK'', which means ``arithmetic-mean thickness and logarithmic-mean hydraulic conductivity''. If the user does not specify a value for CELL\_AVERAGING, then the harmonic-mean method will be used. + +block options +name cvoptions +type record variablecv dewatered +reader urword +optional true +longname vertical conductance options +description none + +block options +name variablecv +in_record true +type keyword +reader urword +longname keyword to activate VARIABLECV option +description keyword to indicate that the vertical conductance will be calculated using the saturated thickness and properties of the overlying cell and the thickness and properties of the underlying cell. If the DEWATERED keyword is also specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. If these keywords are not specified, then the default condition is to calculate the vertical conductance at the start of the simulation using the initial head and the cell properties. The vertical conductance remains constant for the entire simulation. + +block options +name dewatered +in_record true +type keyword +reader urword +optional true +longname keyword to activate DEWATERED option +description If the DEWATERED keyword is specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. + +block options +name newton +type keyword +reader urword +optional true +longname keyword to activate Newton-Raphson +description keyword that activates the Newton-Raphson formulation for groundwater flow between connected, convertible groundwater cells. Cells will not dry when this option is used. + +block options +name xt3d +type keyword +reader urword +optional true +longname keyword to activate XT3D +description keyword that activates the XT3D formulation between the cells connected with this GWF-GWF Exchange. + +block options +name gnc_filerecord +type record gnc6 filein gnc6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name gnc6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname gnc6 keyword +description keyword to specify that record corresponds to a ghost-node correction file. + +block options +name gnc6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname gnc6 input filename +description is the file name for ghost node correction input file. Information for the ghost nodes are provided in the file provided with these keywords. The format for specifying the ghost nodes is the same as described for the GNC Package of the GWF Model. This includes specifying OPTIONS, DIMENSIONS, and GNCDATA blocks. The order of the ghost nodes must follow the same order as the order of the cells in the EXCHANGEDATA block. For the GNCDATA, noden and all of the nodej values are assumed to be located in model 1, and nodem is assumed to be in model 2. + +block options +name mvr_filerecord +type record mvr6 filein mvr6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name mvr6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to a mover file. + +block options +name mvr6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname mvr6 input filename +description is the file name of the water mover input file to apply to this exchange. Information for the water mover are provided in the file provided with these keywords. The format for specifying the water mover information is the same as described for the Water Mover (MVR) Package of the GWF Model, with two exceptions. First, in the PACKAGES block, the model name must be included as a separate string before each package. Second, the appropriate model name must be included before package name 1 and package name 2 in the BEGIN PERIOD block. This allows providers and receivers to be located in both models listed as part of this exchange. + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwf-obstypetable} lists observation type(s) supported by the GWF-GWF package. + +block options +name dev_interfacemodel_on +type keyword +reader urword +optional true +longname activate interface model on exchange +description activates the interface model mechanism for calculating the coefficients at (and possibly near) the exchange. This keyword should only be used for development purposes. +mf6internal dev_ifmod_on + +# --------------------- exg gwfgwf dimensions --------------------- + +block dimensions +name nexg +type integer +reader urword +optional false +longname number of exchanges +description keyword and integer value specifying the number of GWF-GWF exchanges. + + +# --------------------- exg gwfgwf exchangedata --------------------- + +block exchangedata +name exchangedata +type recarray cellidm1 cellidm2 ihc cl1 cl2 hwva aux boundname +shape (nexg) +reader urword +optional false +longname exchange data +description + +block exchangedata +name cellidm1 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of first cell +description is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. +numeric_index true + +block exchangedata +name cellidm2 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of second cell +description is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. +numeric_index true + +block exchangedata +name ihc +type integer +in_record true +tagged false +reader urword +optional false +longname integer flag for connection type +description is an integer flag indicating the direction between node n and all of its m connections. If IHC = 0 then the connection is vertical. If IHC = 1 then the connection is horizontal. If IHC = 2 then the connection is horizontal for a vertically staggered grid. + +block exchangedata +name cl1 +type double precision +in_record true +tagged false +reader urword +optional false +longname connection distance +description is the distance between the center of cell 1 and the its shared face with cell 2. + +block exchangedata +name cl2 +type double precision +in_record true +tagged false +reader urword +optional false +longname connection distance +description is the distance between the center of cell 2 and the its shared face with cell 1. + +block exchangedata +name hwva +type double precision +in_record true +tagged false +reader urword +optional false +longname horizontal cell width or area for vertical flow +description is the horizontal width of the flow connection between cell 1 and cell 2 if IHC $>$ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. + +block exchangedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +longname auxiliary variables +description represents the values of the auxiliary variables for each GWFGWF Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. +mf6internal auxvar + +block exchangedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname exchange boundname +description REPLACE boundname {'{#1}': 'GWF Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-gwfgwt.dfn b/autotest/autotest/temp/dfn/exg-gwfgwt.dfn new file mode 100644 index 00000000..685852dd --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-gwfgwt.dfn @@ -0,0 +1,3 @@ +# --------------------- exg gwfgwt options --------------------- + + diff --git a/autotest/autotest/temp/dfn/exg-gwfprt.dfn b/autotest/autotest/temp/dfn/exg-gwfprt.dfn new file mode 100644 index 00000000..1008a718 --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-gwfprt.dfn @@ -0,0 +1,3 @@ +# --------------------- exg gwfprt options --------------------- + + diff --git a/autotest/autotest/temp/dfn/exg-gwtgwt.dfn b/autotest/autotest/temp/dfn/exg-gwtgwt.dfn new file mode 100644 index 00000000..11af7496 --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-gwtgwt.dfn @@ -0,0 +1,281 @@ +# --------------------- exg gwtgwt options --------------------- +# flopy multi-package + +block options +name gwfmodelname1 +type string +reader urword +optional false +longname keyword to specify name of first corresponding GWF Model +description keyword to specify name of first corresponding GWF Model. In the simulation name file, the GWT6-GWT6 entry contains names for GWT Models (exgmnamea and exgmnameb). The GWT Model with the name exgmnamea must correspond to the GWF Model with the name gwfmodelname1. + +block options +name gwfmodelname2 +type string +reader urword +optional false +longname keyword to specify name of second corresponding GWF Model +description keyword to specify name of second corresponding GWF Model. In the simulation name file, the GWT6-GWT6 entry contains names for GWT Models (exgmnamea and exgmnameb). The GWT Model with the name exgmnameb must correspond to the GWF Model with the name gwfmodelname2. + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description an array of auxiliary variable names. There is no limit on the number of auxiliary variables that can be provided. Most auxiliary variables will not be used by the GWT-GWT Exchange, but they will be available for use by other parts of the program. If an auxiliary variable with the name ``ANGLDEGX'' is found, then this information will be used as the angle (provided in degrees) between the connection face normal and the x axis, where a value of zero indicates that a normal vector points directly along the positive x axis. The connection face normal is a normal vector on the cell face shared between the cell in model 1 and the cell in model 2 pointing away from the model 1 cell. Additional information on ``ANGLDEGX'' is provided in the description of the DISU Package. ANGLDEGX must be specified if dispersion is simulated in the connected GWT models. + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'GWT Exchange'} + +block options +name print_input +type keyword +reader urword +optional true +longname keyword to print input to list file +description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname keyword to print gwfgwf flows to list file +description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname keyword to save GWFGWF flows +description keyword to indicate that cell-by-cell flow terms will be written to the budget file for each model provided that the Output Control for the models are set up with the ``BUDGET SAVE FILE'' option. +mf6internal ipakcb + +block options +name adv_scheme +type string +valid upstream central tvd +reader urword +optional true +longname advective scheme +description scheme used to solve the advection term. Can be upstream, central, or TVD. If not specified, upstream weighting is the default weighting scheme. + +block options +name dsp_xt3d_off +type keyword +shape +reader urword +optional true +longname deactivate xt3d +description deactivate the xt3d method for the dispersive flux and use the faster and less accurate approximation for this exchange. + +block options +name dsp_xt3d_rhs +type keyword +shape +reader urword +optional true +longname xt3d on right-hand side +description add xt3d dispersion terms to right-hand side, when possible, for this exchange. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name mvt_filerecord +type record mvt6 filein mvt6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name mvt6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to a transport mover file. + +block options +name mvt6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname mvt6 input filename +description is the file name of the transport mover input file to apply to this exchange. Information for the transport mover are provided in the file provided with these keywords. + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwt-obstypetable} lists observation type(s) supported by the GWT-GWT package. + +block options +name dev_interfacemodel_on +type keyword +reader urword +optional true +longname activate interface model on exchange +description activates the interface model mechanism for calculating the coefficients at (and possibly near) the exchange. This keyword should only be used for development purposes. +mf6internal dev_ifmod_on + +# --------------------- exg gwtgwt dimensions --------------------- + +block dimensions +name nexg +type integer +reader urword +optional false +longname number of exchanges +description keyword and integer value specifying the number of GWT-GWT exchanges. + + +# --------------------- exg gwtgwt exchangedata --------------------- + +block exchangedata +name exchangedata +type recarray cellidm1 cellidm2 ihc cl1 cl2 hwva aux boundname +shape (nexg) +reader urword +optional false +longname exchange data +description + +block exchangedata +name cellidm1 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of first cell +description is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. +numeric_index true + +block exchangedata +name cellidm2 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of second cell +description is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. +numeric_index true + +block exchangedata +name ihc +type integer +in_record true +tagged false +reader urword +optional false +longname integer flag for connection type +description is an integer flag indicating the direction between node n and all of its m connections. If IHC = 0 then the connection is vertical. If IHC = 1 then the connection is horizontal. If IHC = 2 then the connection is horizontal for a vertically staggered grid. + +block exchangedata +name cl1 +type double precision +in_record true +tagged false +reader urword +optional false +longname connection distance +description is the distance between the center of cell 1 and the its shared face with cell 2. + +block exchangedata +name cl2 +type double precision +in_record true +tagged false +reader urword +optional false +longname connection distance +description is the distance between the center of cell 2 and the its shared face with cell 1. + +block exchangedata +name hwva +type double precision +in_record true +tagged false +reader urword +optional false +longname horizontal cell width or area for vertical flow +description is the horizontal width of the flow connection between cell 1 and cell 2 if IHC $>$ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. + +block exchangedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +longname auxiliary variables +description represents the values of the auxiliary variables for each GWTGWT Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. +mf6internal auxvar + +block exchangedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname exchange boundname +description REPLACE boundname {'{#1}': 'GWT Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-olfgwf.dfn b/autotest/autotest/temp/dfn/exg-olfgwf.dfn new file mode 100644 index 00000000..7cfb29cf --- /dev/null +++ b/autotest/autotest/temp/dfn/exg-olfgwf.dfn @@ -0,0 +1,136 @@ +# --------------------- exg olfgwf options --------------------- +# flopy multi-package + +block options +name print_input +type keyword +reader urword +optional true +longname keyword to print input to list file +description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. +mf6internal ipr_input + +block options +name print_flows +type keyword +reader urword +optional true +longname keyword to print olfgwf flows to list file +description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. +mf6internal ipr_flow + +block options +name fixed_conductance +type keyword +reader urword +optional true +longname keyword to indicate conductance is fixed +description keyword to indicate that the product of the bedleak and cfact input variables in the exchangedata block represents conductance. This conductance is fixed and does not change as a function of head in the surface water and groundwater models. +mf6internal ifixedcond + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwf-obstypetable} lists observation type(s) supported by the SWF-GWF package. + +# --------------------- exg olfgwf dimensions --------------------- + +block dimensions +name nexg +type integer +reader urword +optional false +longname number of exchanges +description keyword and integer value specifying the number of SWF-GWF exchanges. + + +# --------------------- exg olfgwf exchangedata --------------------- + +block exchangedata +name exchangedata +type recarray cellidm1 cellidm2 bedleak cfact +shape (nexg) +reader urword +optional false +longname exchange data +description + +block exchangedata +name cellidm1 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of cell in surface water model +description is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. +numeric_index true + +block exchangedata +name cellidm2 +type integer +in_record true +tagged false +reader urword +optional false +longname cellid of cell in groundwater model +description is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. +numeric_index true + +block exchangedata +name bedleak +type double precision +in_record true +tagged false +reader urword +optional false +longname bed leakance +description is the leakance between the surface water and groundwater. bedleak has dimensions of 1/T and is equal to the hydraulic conductivity of the bed sediments divided by the thickness of the bed sediments. + +block exchangedata +name cfact +type double precision +in_record true +tagged false +reader urword +optional false +longname factor used for conductance calculation +description is the factor used for the conductance calculation. The definition for this parameter depends the type of surface water model and whether or not the fixed\_conductance option is specified. If the fixed\_conductance option is specified, then the hydraulic conductance is calculated as the product of bedleak and cfact. In this case, the conductance is fixed and does not change as a function of the calculated surface water and groundwater head. If the fixed\_conductance option is not specified, then the definition of cfact depends on whether the surface water model represents one-dimensional channel flow or two-dimensional overland flow. If the surface water model represents one-dimensional channel flow, then cfact is the length of the channel cell in the groundwater model cell. If the surface water model represents two-dimensional overland flow, then cfact is the intersection area of the overland flow cell and the underlying groundwater model cell. diff --git a/autotest/autotest/temp/dfn/gwe-adv.dfn b/autotest/autotest/temp/dfn/gwe-adv.dfn new file mode 100644 index 00000000..eaa18993 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-adv.dfn @@ -0,0 +1,19 @@ +# --------------------- gwe adv options --------------------- + +block options +name scheme +type string +valid central upstream tvd +reader urword +optional true +longname advective scheme +description scheme used to solve the advection term. Can be upstream, central, or TVD. If not specified, upstream weighting is the default weighting scheme. + + +block options +name ats_percel +type double precision +reader urword +optional true +longname fractional cell distance used for time step calculation +description fractional cell distance submitted by the ADV Package to the adaptive time stepping (ATS) package. If ATS\_PERCEL is specified and the ATS Package is active, a time step calculation will be made for each cell based on flow through the cell and cell properties. The largest time step will be calculated such that the advective fractional cell distance (ATS\_PERCEL) is not exceeded for any active cell in the grid. This time-step constraint will be submitted to the ATS Package, perhaps with constraints submitted by other packages, in the calculation of the time step. ATS\_PERCEL must be greater than zero. If a value of zero is specified for ATS\_PERCEL the program will automatically reset it to an internal no data value to indicate that time steps should not be subject to this constraint. \ No newline at end of file diff --git a/autotest/autotest/temp/dfn/gwe-cnd.dfn b/autotest/autotest/temp/dfn/gwe-cnd.dfn new file mode 100644 index 00000000..d4b04cff --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-cnd.dfn @@ -0,0 +1,118 @@ +# --------------------- gwe cnd options --------------------- + +block options +name xt3d_off +type keyword +shape +reader urword +optional true +longname deactivate xt3d +description deactivate the xt3d method and use the faster and less accurate approximation. This option may provide a fast and accurate solution under some circumstances, such as when flow aligns with the model grid, there is no mechanical dispersion, or when the longitudinal and transverse dispersivities are equal. This option may also be used to assess the computational demand of the XT3D approach by noting the run time differences with and without this option on. + +block options +name xt3d_rhs +type keyword +shape +reader urword +optional true +longname xt3d on right-hand side +description add xt3d terms to right-hand side, when possible. This option uses less memory, but may require more iterations. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwe cnd griddata --------------------- + +block griddata +name alh +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname longitudinal dispersivity in horizontal direction +description longitudinal dispersivity in horizontal direction. If flow is strictly horizontal, then this is the longitudinal dispersivity that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. + +block griddata +name alv +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname longitudinal dispersivity in vertical direction +description longitudinal dispersivity in vertical direction. If flow is strictly vertical, then this is the longitudinal dispsersivity value that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ALH. + +block griddata +name ath1 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity in horizontal direction +description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the second ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the y direction. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. + +block griddata +name ath2 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity in horizontal direction +description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the third ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the z direction. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH1. + +block griddata +name atv +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity when flow is in vertical direction +description transverse dispersivity when flow is in vertical direction. If flow is strictly vertical and directed in the z direction, then this value controls spreading in the x and y directions. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH2. + +block griddata +name ktw +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname thermal conductivity of the simulated fluid +description thermal conductivity of the simulated fluid. Note that the CND Package does not account for the tortuosity of the flow paths when solving for the conductive spread of heat. If tortuosity plays an important role in the thermal conductivity calculation, its effect should be reflected in the value specified for KTW. + +block griddata +name kts +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname thermal conductivity of the aquifer material +description thermal conductivity of the solid aquifer material + diff --git a/autotest/autotest/temp/dfn/gwe-ctp.dfn b/autotest/autotest/temp/dfn/gwe-ctp.dfn new file mode 100644 index 00000000..d8930e59 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-ctp.dfn @@ -0,0 +1,213 @@ +# --------------------- gwe ctp options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'temperature value'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'constant temperature'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'constant temperature'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'constant temperature'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save constant temperature flows to budget file +description REPLACE save_flows {'{#1}': 'constant temperature'} +mf6internal ipakcb + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname time series keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'CTP', '{#2}': '\\ref{table:gwe-obstypetable}'} + + +# --------------------- gwe ctp dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of constant temperatures +description REPLACE maxbound {'{#1}': 'constant temperature'} + + +# --------------------- gwe ctp period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid temp aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name temp +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname constant temperature value +description is the constant temperature value. 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. +mf6internal tspvar + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'constant temperature'} +mf6internal auxvar + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname constant temperature name +description REPLACE boundname {'{#1}': 'constant temperature'} diff --git a/autotest/autotest/temp/dfn/gwe-dis.dfn b/autotest/autotest/temp/dfn/gwe-dis.dfn new file mode 100644 index 00000000..fea21df3 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-dis.dfn @@ -0,0 +1,239 @@ +# --------------------- gwe dis options --------------------- +# mf6 subpackage utl-ncf + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position of the model grid origin +description x-position of the lower-left corner of the model grid. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position of the model grid origin +description y-position of the lower-left corner of the model grid. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the lower-left corner of the model grid. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name ncf_filerecord +type record ncf6 filein ncf6_filename +reader urword +tagged true +optional true +longname +description + +block options +name ncf6 +type keyword +in_record true +reader urword +tagged true +optional false +longname ncf keyword +description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ncf6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of NCF information +description defines a NetCDF configuration (NCF) input file. +extended true + +# --------------------- gwe dis dimensions --------------------- + +block dimensions +name nlay +type integer +reader urword +optional false +longname number of layers +description is the number of layers in the model grid. +default_value 1 + +block dimensions +name nrow +type integer +reader urword +optional false +longname number of rows +description is the number of rows in the model grid. +default_value 2 + +block dimensions +name ncol +type integer +reader urword +optional false +longname number of columns +description is the number of columns in the model grid. +default_value 2 + +# --------------------- gwe dis griddata --------------------- + +block griddata +name delr +type double precision +shape (ncol) +reader readarray +netcdf true +longname spacing along a row +description is the column spacing in the row direction. +default_value 1.0 + +block griddata +name delc +type double precision +shape (nrow) +reader readarray +netcdf true +longname spacing along a column +description is the row spacing in the column direction. +default_value 1.0 + +block griddata +name top +type double precision +shape (ncol, nrow) +reader readarray +netcdf true +longname cell top elevation +description is the top elevation for each cell in the top model layer. +default_value 1.0 + +block griddata +name botm +type double precision +shape (ncol, nrow, nlay) +reader readarray +layered true +netcdf true +longname cell bottom elevation +description is the bottom elevation for each cell. +default_value 0. + +block griddata +name idomain +type integer +shape (ncol, nrow, nlay) +reader readarray +layered true +netcdf true +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. + + diff --git a/autotest/autotest/temp/dfn/gwe-disu.dfn b/autotest/autotest/temp/dfn/gwe-disu.dfn new file mode 100644 index 00000000..aff179ec --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-disu.dfn @@ -0,0 +1,337 @@ +# --------------------- gwe disu options --------------------- + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position origin of the model grid coordinate system +description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position origin of the model grid coordinate system +description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name vertical_offset_tolerance +type double precision +reader urword +optional true +default_value 0.0 +longname vertical length dimension for top and bottom checking +description checks are performed to ensure that the top of a cell is not higher than the bottom of an overlying cell. This option can be used to specify the tolerance that is used for checking. If top of a cell is above the bottom of an overlying cell by a value less than this tolerance, then the program will not terminate with an error. The default value is zero. This option should generally not be used. +mf6internal voffsettol + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +# --------------------- gwe disu dimensions --------------------- + +block dimensions +name nodes +type integer +reader urword +optional false +longname number of layers +description is the number of cells in the model grid. + +block dimensions +name nja +type integer +reader urword +optional false +longname number of columns +description is the sum of the number of connections and NODES. When calculating the total number of connections, the connection between cell n and cell m is considered to be different from the connection between cell m and cell n. Thus, NJA is equal to the total number of connections, including n to m and m to n, and the total number of cells. + +block dimensions +name nvert +type integer +reader urword +optional true +longname number of vertices +description is the total number of (x, y) vertex pairs used to define the plan-view shape of each cell in the model grid. If NVERT is not specified or is specified as zero, then the VERTICES and CELL2D blocks below are not read. NVERT and the accompanying VERTICES and CELL2D blocks should be specified for most simulations. If the XT3D or SAVE\_SPECIFIC\_DISCHARGE options are specified in the NPF Package, then this information is required. + +# --------------------- gwe disu griddata --------------------- + +block griddata +name top +type double precision +shape (nodes) +reader readarray +longname cell top elevation +description is the top elevation for each cell in the model grid. + +block griddata +name bot +type double precision +shape (nodes) +reader readarray +longname cell bottom elevation +description is the bottom elevation for each cell. + +block griddata +name area +type double precision +shape (nodes) +reader readarray +longname cell surface area +description is the cell surface area (in plan view). + +block griddata +name idomain +type integer +shape (nodes) +reader readarray +layered false +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1 or greater, the cell exists in the simulation. IDOMAIN values of -1 cannot be specified for the DISU Package. + +# --------------------- gwe disu connectiondata --------------------- + +block connectiondata +name iac +type integer +shape (nodes) +reader readarray +longname number of cell connections +description is the number of connections (plus 1) for each cell. The sum of all the entries in IAC must be equal to NJA. + +block connectiondata +name ja +type integer +shape (nja) +reader readarray +longname grid connectivity +description is a list of cell number (n) followed by its connecting cell numbers (m) for each of the m cells connected to cell n. The number of values to provide for cell n is IAC(n). This list is sequentially provided for the first to the last cell. The first value in the list must be cell n itself, and the remaining cells must be listed in an increasing order (sorted from lowest number to highest). Note that the cell and its connections are only supplied for the GWE cells and their connections to the other GWE cells. Also note that the JA list input may be divided such that every node and its connectivity list can be on a separate line for ease in readability of the file. To further ease readability of the file, the node number of the cell whose connectivity is subsequently listed, may be expressed as a negative number, the sign of which is subsequently converted to positive by the code. +numeric_index true +jagged_array iac + +block connectiondata +name ihc +type integer +shape (nja) +reader readarray +longname connection type +description is an index array indicating the direction between node n and all of its m connections. If IHC = 0 then cell n and cell m are connected in the vertical direction. Cell n overlies cell m if the cell number for n is less than m; cell m overlies cell n if the cell number for m is less than n. If IHC = 1 then cell n and cell m are connected in the horizontal direction. If IHC = 2 then cell n and cell m are connected in the horizontal direction, and the connection is vertically staggered. A vertically staggered connection is one in which a cell is horizontally connected to more than one cell in a horizontal connection. +jagged_array iac + +block connectiondata +name cl12 +type double precision +shape (nja) +reader readarray +longname connection lengths +description is the array containing connection lengths between the center of cell n and the shared face with each adjacent m cell. +jagged_array iac + +block connectiondata +name hwva +type double precision +shape (nja) +reader readarray +longname connection lengths +description is a symmetric array of size NJA. For horizontal connections, entries in HWVA are the horizontal width perpendicular to flow. For vertical connections, entries in HWVA are the vertical area for flow. Thus, values in the HWVA array contain dimensions of both length and area. Entries in the HWVA array have a one-to-one correspondence with the connections specified in the JA array. Likewise, there is a one-to-one correspondence between entries in the HWVA array and entries in the IHC array, which specifies the connection type (horizontal or vertical). Entries in the HWVA array must be symmetric; the program will terminate with an error if the value for HWVA for an n to m connection does not equal the value for HWVA for the corresponding n to m connection. +jagged_array iac + +block connectiondata +name angldegx +type double precision +optional true +shape (nja) +reader readarray +longname angle of face normal to connection +description is the angle (in degrees) between the horizontal x-axis and the outward normal to the face between a cell and its connecting cells. The angle varies between zero and 360.0 degrees, where zero degrees points in the positive x-axis direction, and 90 degrees points in the positive y-axis direction. ANGLDEGX is only needed if horizontal anisotropy is specified in the NPF Package, if the XT3D option is used in the NPF Package, or if the SAVE\_SPECIFIC\_DISCHARGE option is specified in the NPF Package. ANGLDEGX does not need to be specified if these conditions are not met. ANGLDEGX is of size NJA; values specified for vertical connections and for the diagonal position are not used. Note that ANGLDEGX is read in degrees, which is different from MODFLOW-USG, which reads a similar variable (ANGLEX) in radians. +jagged_array iac + +# --------------------- gwe disu vertices --------------------- + +block vertices +name vertices +type recarray iv xv yv +shape (nvert) +reader urword +optional false +longname vertices data +description + +block vertices +name iv +type integer +in_record true +tagged false +reader urword +optional false +longname vertex number +description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. +numeric_index true + +block vertices +name xv +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for vertex +description is the x-coordinate for the vertex. + +block vertices +name yv +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for vertex +description is the y-coordinate for the vertex. + + +# --------------------- gwe disu cell2d --------------------- + +block cell2d +name cell2d +type recarray icell2d xc yc ncvert icvert +shape (nodes) +reader urword +optional false +longname cell2d data +description + +block cell2d +name icell2d +type integer +in_record true +tagged false +reader urword +optional false +longname cell2d number +description is the cell2d number. Records in the CELL2D block must be listed in consecutive order from 1 to NODES. +numeric_index true + +block cell2d +name xc +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for cell center +description is the x-coordinate for the cell center. + +block cell2d +name yc +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for cell center +description is the y-coordinate for the cell center. + +block cell2d +name ncvert +type integer +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. + +block cell2d +name icvert +type integer +shape (ncvert) +in_record true +tagged false +reader urword +optional false +longname array of vertex numbers +description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. +numeric_index true diff --git a/autotest/autotest/temp/dfn/gwe-disv.dfn b/autotest/autotest/temp/dfn/gwe-disv.dfn new file mode 100644 index 00000000..4ed1e0a9 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-disv.dfn @@ -0,0 +1,320 @@ +# --------------------- gwe disv options --------------------- +# mf6 subpackage utl-ncf + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position origin of the model grid coordinate system +description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position origin of the model grid coordinate system +description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name ncf_filerecord +type record ncf6 filein ncf6_filename +reader urword +tagged true +optional true +longname +description + +block options +name ncf6 +type keyword +in_record true +reader urword +tagged true +optional false +longname ncf keyword +description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ncf6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of NCF information +description defines a NetCDF configuration (NCF) input file. +extended true + +# --------------------- gwe disv dimensions --------------------- + +block dimensions +name nlay +type integer +reader urword +optional false +longname number of layers +description is the number of layers in the model grid. + +block dimensions +name ncpl +type integer +reader urword +optional false +longname number of cells per layer +description is the number of cells per layer. This is a constant value for the grid and it applies to all layers. + +block dimensions +name nvert +type integer +reader urword +optional false +longname number of columns +description is the total number of (x, y) vertex pairs used to characterize the horizontal configuration of the model grid. + +# --------------------- gwe disv griddata --------------------- + +block griddata +name top +type double precision +shape (ncpl) +reader readarray +netcdf true +longname model top elevation +description is the top elevation for each cell in the top model layer. + +block griddata +name botm +type double precision +shape (ncpl, nlay) +reader readarray +layered true +netcdf true +longname model bottom elevation +description is the bottom elevation for each cell. + +block griddata +name idomain +type integer +shape (ncpl, nlay) +reader readarray +layered true +netcdf true +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. + + +# --------------------- gwe disv vertices --------------------- + +block vertices +name vertices +type recarray iv xv yv +shape (nvert) +reader urword +optional false +longname vertices data +description + +block vertices +name iv +type integer +in_record true +tagged false +reader urword +optional false +longname vertex number +description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. +numeric_index true + +block vertices +name xv +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for vertex +description is the x-coordinate for the vertex. + +block vertices +name yv +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for vertex +description is the y-coordinate for the vertex. + + +# --------------------- gwe disv cell2d --------------------- + +block cell2d +name cell2d +type recarray icell2d xc yc ncvert icvert +shape (ncpl) +reader urword +optional false +longname cell2d data +description + +block cell2d +name icell2d +type integer +in_record true +tagged false +reader urword +optional false +longname cell2d number +description is the CELL2D number. Records in the CELL2D block must be listed in consecutive order from the first to the last. +numeric_index true + +block cell2d +name xc +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for cell center +description is the x-coordinate for the cell center. + +block cell2d +name yc +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for cell center +description is the y-coordinate for the cell center. + +block cell2d +name ncvert +type integer +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. + +block cell2d +name icvert +type integer +shape (ncvert) +in_record true +tagged false +reader urword +optional false +longname array of vertex numbers +description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. Cells that are connected must share vertices. +numeric_index true diff --git a/autotest/autotest/temp/dfn/gwe-esl.dfn b/autotest/autotest/temp/dfn/gwe-esl.dfn new file mode 100644 index 00000000..e12bb453 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-esl.dfn @@ -0,0 +1,211 @@ +# --------------------- gwe esl options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'energy loading rate'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'energy source loading'} + +block options +name print_input +type keyword +reader urword +optional true +mf6internal iprpak +longname print input to listing file +description REPLACE print_input {'{#1}': 'energy source loading'} + +block options +name print_flows +type keyword +reader urword +optional true +mf6internal iprflow +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'energy source loading'} + +block options +name save_flows +type keyword +reader urword +optional true +mf6internal ipakcb +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'energy source loading'} + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'ESL', '{#2}': '\\ref{table:gwe-obstypetable}'} + +# --------------------- gwe esl dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of sources +description REPLACE maxbound {'{#1}': 'source'} + + +# --------------------- gwe esl period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid senerrate aux boundname +shape (maxbound) +reader urword +mf6internal spd +longname +description + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name senerrate +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname energy source loading rate +description is the energy source loading rate. A positive value indicates addition of energy and a negative value indicates removal of energy. 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +mf6internal auxvar +longname auxiliary variables +description REPLACE aux {'{#1}': 'energy source'} + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname well name +description REPLACE boundname {'{#1}': 'energy source'} diff --git a/autotest/autotest/temp/dfn/gwe-est.dfn b/autotest/autotest/temp/dfn/gwe-est.dfn new file mode 100644 index 00000000..0ce8e2c4 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-est.dfn @@ -0,0 +1,108 @@ +# --------------------- gwe est options --------------------- + +block options +name save_flows +type keyword +reader urword +optional true +longname save calculated flows to budget file +description REPLACE save_flows {'{#1}': 'EST'} + +block options +name zero_order_decay_water +type keyword +reader urword +optional true +mf6internal ord0_decay_water +longname activate zero-order decay in aqueous phase +description is a text keyword to indicate that zero-order decay will occur in the aqueous phase. That is, decay occurs in the water and is a rate per volume of water only, not per volume of aquifer (i.e., grid cell). Use of this keyword requires that DECAY\_WATER is specified in the GRIDDATA block. + +block options +name zero_order_decay_solid +type keyword +reader urword +optional true +mf6internal ord0_decay_solid +longname activate zero-order decay in solid phase +description is a text keyword to indicate that zero-order decay will occur in the solid phase. That is, decay occurs in the solid and is a rate per mass (not volume) of solid only. Use of this keyword requires that DECAY\_SOLID is specified in the GRIDDATA block. + +block options +name density_water +type double precision +reader urword +optional true +longname density of water +description density of water used by calculations related to heat storage and conduction. This value is set to 1,000 kg/m3 if no overriding value is specified. A user-specified value should be provided for models that use units other than kilograms and meters or if it is necessary to use a value other than the default. +default_value 1000.0 +mf6internal rhow + +block options +name heat_capacity_water +type double precision +reader urword +optional true +longname heat capacity of water +description heat capacity of water used by calculations related to heat storage and conduction. This value is set to 4,184 J/kg/C if no overriding value is specified. A user-specified value should be provided for models that use units other than kilograms, joules, and degrees Celsius or it is necessary to use a value other than the default. +default_value 4184.0 +mf6internal cpw + +block options +name latent_heat_vaporization +type double precision +reader urword +optional true +longname latent heat of vaporization +description latent heat of vaporization is the amount of energy that is required to convert a given quantity of liquid into a gas and is associated with evaporative cooling. While the EST package does not simulate evaporation, multiple other packages in a GWE simulation may. To avoid having to specify the latent heat of vaporization in multiple packages, it is specified in a single location and accessed wherever it is needed. For example, evaporation may occur from the surface of streams or lakes and the energy consumed by the change in phase would be needed in both the SFE and LKE packages. This value is set to 2,453,500 J/kg if no overriding value is specified. A user-specified value should be provided for models that use units other than joules and kilograms or if it is necessary to use a value other than the default. +default_value 2453500.0 +mf6internal latheatvap + +# --------------------- gwe est griddata --------------------- + +block griddata +name porosity +type double precision +shape (nodes) +reader readarray +layered true +longname porosity +description is the mobile domain porosity, defined as the mobile domain pore volume per mobile domain volume. The GWE model does not support the concept of an immobile domain in the context of heat transport. + +block griddata +name decay_water +type double precision +shape (nodes) +reader readarray +layered true +optional true +longname aqueous phase decay rate coefficient +description is the rate coefficient for zero-order decay for the aqueous phase of the mobile domain. A negative value indicates heat (energy) production. The dimensions of zero-order decay in the aqueous phase are energy per length cubed (volume of water) per time. Zero-order decay in the aqueous phase will have no effect on simulation results unless ZERO\_ORDER\_DECAY\_WATER is specified in the options block. + +block griddata +name decay_solid +type double precision +shape (nodes) +reader readarray +layered true +optional true +longname solid phase decay rate coefficient +description is the rate coefficient for zero-order decay for the solid phase. A negative value indicates heat (energy) production. The dimensions of zero-order decay in the solid phase are energy per mass of solid per time. Zero-order decay in the solid phase will have no effect on simulation results unless ZERO\_ORDER\_DECAY\_SOLID is specified in the options block. + +block griddata +name heat_capacity_solid +type double precision +shape (nodes) +reader readarray +layered true +longname heat capacity of the aquifer material +description is the mass-based heat capacity of dry solids (aquifer material). For example, units of J/kg/C may be used (or equivalent). +mf6internal cps + +block griddata +name density_solid +type double precision +shape (nodes) +reader readarray +layered true +longname density of aquifer material +description is a user-specified value of the density of aquifer material not considering the voids. Value will remain fixed for the entire simulation. For example, if working in SI units, values may be entered as kilograms per cubic meter. +mf6internal rhos diff --git a/autotest/autotest/temp/dfn/gwe-fmi.dfn b/autotest/autotest/temp/dfn/gwe-fmi.dfn new file mode 100644 index 00000000..98cc2397 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-fmi.dfn @@ -0,0 +1,59 @@ +# --------------------- gwe fmi options --------------------- + +block options +name save_flows +type keyword +reader urword +optional true +longname save calculated flow imbalance correction to budget file +description REPLACE save_flows {'{#1}': 'FMI'} + +block options +name flow_imbalance_correction +type keyword +reader urword +optional true +mf6internal imbalancecorrect +longname correct for flow imbalance +description correct for an imbalance in flows by assuming that any residual flow error comes in or leaves at the temperature of the cell. When this option is activated, the GWE Model budget written to the listing file will contain two additional entries: FLOW-ERROR and FLOW-CORRECTION. These two entries will be equal but opposite in sign. The FLOW-CORRECTION term is a mass flow that is added to offset the error caused by an imprecise flow balance. If these terms are not relatively small, the flow model should be rerun with stricter convergence tolerances. + +# --------------------- gwe fmi packagedata --------------------- + +block packagedata +name packagedata +type recarray flowtype filein fname +reader urword +optional true +longname flowtype list +description + +block packagedata +name flowtype +in_record true +type string +tagged false +reader urword +longname flow type +description is the word GWFBUDGET, GWFHEAD, GWFGRID, GWFMOVER or the name of an advanced GWF stress package from a previous model run. If GWFBUDGET is specified, then the corresponding file must be a budget file. If GWFHEAD is specified, the file must be a head file. If GWFGRID is specified, the file must be a binary grid file. If GWFMOVER is specified, the file must be a mover file. If an advanced GWF stress package name appears then the corresponding file must be the budget file saved by a LAK, SFR, MAW or UZF Package. + +block packagedata +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block packagedata +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing flows. The path to the file should be included if the file is not located in the folder where the program was run. + diff --git a/autotest/autotest/temp/dfn/gwe-ic.dfn b/autotest/autotest/temp/dfn/gwe-ic.dfn new file mode 100644 index 00000000..d9aa11bd --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-ic.dfn @@ -0,0 +1,33 @@ +# --------------------- gwe ic options --------------------- + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwe ic griddata --------------------- + +block griddata +name strt +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname starting temperature +description is the initial (starting) temperature---that is, the temperature at the beginning of the GWE Model simulation. STRT must be specified for all GWE Model simulations. One value is read for every model cell. +default_value 0.0 diff --git a/autotest/autotest/temp/dfn/gwe-lke.dfn b/autotest/autotest/temp/dfn/gwe-lke.dfn new file mode 100644 index 00000000..fc3d16cd --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-lke.dfn @@ -0,0 +1,481 @@ +# --------------------- gwe lke options --------------------- +# flopy multi-package + +block options +name flow_package_name +type string +shape +reader urword +optional true +longname keyword to specify name of corresponding flow package +description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWE name file). + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} + +block options +name flow_package_auxiliary_name +type string +shape +reader urword +optional true +longname keyword to specify name of temperature auxiliary variable in flow package +description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated temperatures from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'lake'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'lake'} + +block options +name print_temperature +type keyword +reader urword +optional true +longname print calculated temperatures to listing file +description REPLACE print_temperature {'{#1}': 'lake', '{#2}': 'temperature', '{#3}': 'TEMPERATURE'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'lake'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save lake flows to budget file +description REPLACE save_flows {'{#1}': 'lake'} + +block options +name temperature_filerecord +type record temperature fileout tempfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name temperature +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname stage keyword +description keyword to specify that record corresponds to temperature. + +block options +name tempfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write temperature information. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'LKE', '{#2}': '\\ref{table:gwe-obstypetable}'} + + +# --------------------- gwe lke packagedata --------------------- + +block packagedata +name packagedata +type recarray lakeno strt ktf rbthcnd aux boundname +shape (maxbound) +reader urword +longname +description + +block packagedata +name lakeno +type integer +shape +tagged false +in_record true +reader urword +longname lake number for this entry +description integer value that defines the lake number associated with the specified PACKAGEDATA data on the line. LAKENO must be greater than zero and less than or equal to NLAKES. Lake information must be specified for every lake or the program will terminate with an error. The program will also terminate with an error if information for a lake is specified more than once. +numeric_index true + +block packagedata +name strt +type double precision +shape +tagged false +in_record true +reader urword +longname starting lake temperature +description real value that defines the starting temperature for the lake. + +block packagedata +name ktf +type double precision +shape +tagged false +in_record true +reader urword +longname boundary thermal conductivity +description is the thermal conductivity of the material between the aquifer cell and the lake. The thickness of the material is defined by the variable RBTHCND. + +block packagedata +name rbthcnd +type double precision +shape +tagged false +in_record true +reader urword +longname streambed thickness +description real value that defines the thickness of the lakebed material through which conduction occurs. Must be greater than 0. + +block packagedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +time_series true +optional true +longname auxiliary variables +description REPLACE aux {'{#1}': 'lake'} + +block packagedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname lake name +description REPLACE boundname {'{#1}': 'lake'} + + +# --------------------- gwe lke period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name lakeperioddata +type recarray lakeno laksetting +shape +reader urword +longname +description + +block period +name lakeno +type integer +shape +tagged false +in_record true +reader urword +longname lake number for this entry +description integer value that defines the lake number associated with the specified PERIOD data on the line. LAKENO must be greater than zero and less than or equal to NLAKES. +numeric_index true + +block period +name laksetting +type keystring status temperature rainfall evaporation runoff ext-inflow auxiliaryrecord +shape +tagged false +in_record true +reader urword +longname +description line of information that is parsed into a keyword and values. Keyword values that can be used to start the LAKSETTING string include: STATUS, TEMPERATURE, RAINFALL, EVAPORATION, RUNOFF, and AUXILIARY. These settings are used to assign the temperature associated with the corresponding flow terms. Temperatures cannot be specified for all flow terms. For example, the Lake Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the lake at the calculated temperature of the lake. + +block period +name status +type string +shape +tagged true +in_record true +reader urword +longname lake temperature status +description keyword option to define lake status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that temperature will be calculated for the lake. If a lake is inactive, then there will be no energy fluxes into or out of the lake and the inactive value will be written for the lake temperature. If a lake is constant, then the temperature for the lake will be fixed at the user specified value. + +block period +name temperature +type string +shape +tagged true +in_record true +time_series true +reader urword +longname lake temperature +description real or character value that defines the temperature for the lake. The specified TEMPERATURE is only applied if the lake is a constant temperature lake. 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. + +block period +name rainfall +type string +shape +tagged true +in_record true +reader urword +time_series true +longname rainfall temperature +description real or character value that defines the rainfall temperature for the lake. 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. + +block period +name evaporation +type string +shape +tagged true +in_record true +reader urword +time_series true +longname evaporation temperature +description use of the EVAPORATION keyword is allowed in the LKE package; however, the specified value is not currently used in LKE calculations. Instead, the latent heat of evaporation is multiplied by the simulated evaporation rate for determining the thermal energy lost from a stream reach. + + +block period +name runoff +type string +shape +tagged true +in_record true +reader urword +time_series true +longname runoff temperature +description real or character value that defines the temperature of runoff for the lake. Users are free to use whatever temperature scale they want, which might include negative temperatures. 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. + +block period +name ext-inflow +type string +shape +tagged true +in_record true +reader urword +time_series true +longname ext-inflow temperature +description real or character value that defines the temperature of external inflow for the lake. Users are free to use whatever temperature scale they want, which might include negative temperatures. 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. + +block period +name auxiliaryrecord +type record auxiliary auxname auxval +shape +tagged +in_record true +reader urword +longname +description + +block period +name auxiliary +type keyword +shape +in_record true +reader urword +longname +description keyword for specifying auxiliary variable. + +block period +name auxname +type string +shape +tagged false +in_record true +reader urword +longname +description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. + +block period +name auxval +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname auxiliary variable value +description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwe-mve.dfn b/autotest/autotest/temp/dfn/gwe-mve.dfn new file mode 100644 index 00000000..e67e76ba --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-mve.dfn @@ -0,0 +1,106 @@ +# --------------------- gwe mve options --------------------- +# flopy subpackage mve_filerecord mve perioddata perioddata +# flopy parent_name_type parent_model_or_package MFModel/MFPackage + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'mover'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'lake'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save lake flows to budget file +description REPLACE save_flows {'{#1}': 'lake'} + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + + diff --git a/autotest/autotest/temp/dfn/gwe-mwe.dfn b/autotest/autotest/temp/dfn/gwe-mwe.dfn new file mode 100644 index 00000000..d751bbd2 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-mwe.dfn @@ -0,0 +1,447 @@ +# --------------------- gwe mwe options --------------------- +# flopy multi-package + +block options +name flow_package_name +type string +shape +reader urword +optional true +longname keyword to specify name of corresponding flow package +description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWE name file). + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} + +block options +name flow_package_auxiliary_name +type string +shape +reader urword +optional true +longname keyword to specify name of temperature auxiliary variable in flow package +description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated temperatures from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'well'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'well'} + +block options +name print_temperature +type keyword +reader urword +optional true +longname print calculated temperatures to listing file +description REPLACE print_temperature {'{#1}': 'well', '{#2}': 'temperature', '{#3}': 'TEMPERATURE'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'well'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'well'} + +block options +name temperature_filerecord +type record temperature fileout tempfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name temperature +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname stage keyword +description keyword to specify that record corresponds to temperature. + +block options +name tempfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write temperature information. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'MWE', '{#2}': '\\ref{table:gwe-obstypetable}'} + + +# --------------------- gwe mwe packagedata --------------------- + +block packagedata +name packagedata +type recarray mawno strt ktf fthk aux boundname +shape (maxbound) +reader urword +longname +description + +block packagedata +name mawno +type integer +shape +tagged false +in_record true +reader urword +longname well number for this entry +description integer value that defines the well number associated with the specified PACKAGEDATA data on the line. MAWNO must be greater than zero and less than or equal to NMAWWELLS. Well information must be specified for every well or the program will terminate with an error. The program will also terminate with an error if information for a well is specified more than once. +numeric_index true + +block packagedata +name strt +type double precision +shape +tagged false +in_record true +reader urword +longname starting well temperature +description real value that defines the starting temperature for the well. + +block packagedata +name ktf +type double precision +shape +tagged false +in_record true +reader urword +longname thermal conductivity of the feature +description is the thermal conductivity of the material between the aquifer cell and the feature. The thickness of the material is defined by the variable FTHK. + +block packagedata +name fthk +type double precision +shape +tagged false +in_record true +reader urword +longname thickness of the well feature +description real value that defines the thickness of the material through which conduction occurs. Must be greater than 0. + +block packagedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +time_series true +optional true +longname auxiliary variables +description REPLACE aux {'{#1}': 'well'} + +block packagedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname well name +description REPLACE boundname {'{#1}': 'well'} + + +# --------------------- gwe mwe period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name mweperioddata +type recarray mawno mwesetting +shape +reader urword +longname +description + +block period +name mawno +type integer +shape +tagged false +in_record true +reader urword +longname well number for this entry +description integer value that defines the well number associated with the specified PERIOD data on the line. MAWNO must be greater than zero and less than or equal to NMAWWELLS. +numeric_index true + +block period +name mwesetting +type keystring status temperature rate auxiliaryrecord +shape +tagged false +in_record true +reader urword +longname +description line of information that is parsed into a keyword and values. Keyword values that can be used to start the MWESETTING string include: STATUS, TEMPERATURE, RAINFALL, EVAPORATION, RUNOFF, and AUXILIARY. These settings are used to assign the temperature of associated with the corresponding flow terms. Temperatures cannot be specified for all flow terms. For example, the Multi-Aquifer Well Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the well at the calculated temperature of the well. + +block period +name status +type string +shape +tagged true +in_record true +reader urword +longname well temperature status +description keyword option to define well status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that temperature will be calculated for the well. If a well is inactive, then there will be no solute mass fluxes into or out of the well and the inactive value will be written for the well temperature. If a well is constant, then the temperature for the well will be fixed at the user specified value. + +block period +name temperature +type string +shape +tagged true +in_record true +time_series true +reader urword +longname well temperature +description real or character value that defines the temperature for the well. The specified TEMPERATURE is only applied if the well is a constant temperature well. 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. + +block period +name rate +type string +shape +tagged true +in_record true +reader urword +time_series true +longname well injection temperature +description real or character value that defines the injection temperature $(e.g.,\:^{\circ}C\:or\:^{\circ}F)$ for the well. 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. + +block period +name auxiliaryrecord +type record auxiliary auxname auxval +shape +tagged +in_record true +reader urword +longname +description + +block period +name auxiliary +type keyword +shape +in_record true +reader urword +longname +description keyword for specifying auxiliary variable. + +block period +name auxname +type string +shape +tagged false +in_record true +reader urword +longname +description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. + +block period +name auxval +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname auxiliary variable value +description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwe-nam.dfn b/autotest/autotest/temp/dfn/gwe-nam.dfn new file mode 100644 index 00000000..a3e89b0e --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-nam.dfn @@ -0,0 +1,210 @@ +# --------------------- gwe nam options --------------------- + +block options +name list +type string +reader urword +optional true +preserve_case true +longname name of listing file +description is name of the listing file to create for this GWE model. If not specified, then the name of the list file will be the basename of the GWE model name file and the ``.lst'' extension. For example, if the GWE name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'all model stress package'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'all model package'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save flows for all packages to budget file +description REPLACE save_flows {'{#1}': 'all model package'} + +block options +name dependent_variable_scaling +type keyword +reader urword +optional true +longname flag to scale X and RHS +description flag to scale X and RHS to avoid very large positive or negative dependent variable values +mf6internal idv_scale + +block options +name nc_mesh2d_filerecord +type record netcdf_mesh2d fileout ncmesh2dfile +shape +reader urword +tagged true +optional true +longname +description NetCDF layered mesh fileout record. +mf6internal ncmesh2drec + +block options +name netcdf_mesh2d +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a layered mesh NetCDF file. +extended true + +block options +name nc_structured_filerecord +type record netcdf_structured fileout ncstructfile +shape +reader urword +tagged true +optional true +longname +description NetCDF structured fileout record. +mf6internal ncstructrec + +block options +name netcdf_structured +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a structured NetCDF file. +mf6internal netcdf_struct +extended true + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name ncmesh2dfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF ugrid layered mesh output file. +extended true + +block options +name ncstructfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF structured output file. +extended true + +block options +name nc_filerecord +type record netcdf filein netcdf_filename +reader urword +tagged true +optional true +longname +description NetCDF filerecord + +block options +name netcdf +type keyword +in_record true +reader urword +tagged true +optional false +longname netcdf keyword +description keyword to specify that record corresponds to a NetCDF input file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name netcdf_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname netcdf input filename +description defines a NetCDF input file. +mf6internal netcdf_fname +extended true + +# --------------------- gwe nam packages --------------------- + +block packages +name packages +type recarray ftype fname pname +reader urword +optional false +longname package list +description + +block packages +name ftype +in_record true +type string +tagged false +reader urword +longname package type +description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwe}. Ftype may be entered in any combination of uppercase and lowercase. + +block packages +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. + +block packages +name pname +in_record true +type string +tagged false +reader urword +optional true +longname user name for package +description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWE Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. + diff --git a/autotest/autotest/temp/dfn/gwe-oc.dfn b/autotest/autotest/temp/dfn/gwe-oc.dfn new file mode 100644 index 00000000..35f14794 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwe-oc.dfn @@ -0,0 +1,318 @@ +# --------------------- gwe oc options --------------------- + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +mf6internal budfilerec +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +mf6internal budcsvfilerec +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name temperature_filerecord +type record temperature fileout temperaturefile +shape +reader urword +tagged true +optional true +mf6internal tempfilerec +longname +description + +block options +name temperature +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname temperature keyword +description keyword to specify that record corresponds to temperature. + +block options +name temperaturefile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +mf6internal tempfile +description name of the output file to write temperature information. + +block options +name temperatureprintrecord +type record temperature print_format formatrecord +shape +reader urword +optional true +mf6internal tempprintrec +longname +description + +block options +name print_format +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to indicate that a print format follows +description keyword to specify format for printing to the listing file. + +block options +name formatrecord +type record columns width digits format +shape +in_record true +reader urword +tagged +optional false +longname +description + +block options +name columns +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of columns +description number of columns for writing data. + +block options +name width +type integer +shape +in_record true +reader urword +tagged true +optional +longname width for each number +description width for writing each number. + +block options +name digits +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of digits +description number of digits to use for writing a number. + +block options +name format +type string +shape +in_record true +reader urword +tagged false +optional false +longname write format +description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. + + +# --------------------- gwe oc period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name saverecord +type record save rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name save +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be saved this stress period. + +block period +name printrecord +type record print rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name print +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be printed this stress period. + +block period +name rtype +type string +shape +in_record true +reader urword +tagged false +optional false +longname record type +description type of information to save or print. Can be BUDGET or TEMPERATURE. + +block period +name ocsetting +type keystring all first last frequency steps +shape +tagged false +in_record true +reader urword +longname +description specifies the steps for which the data will be saved. + +block period +name all +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for all time steps in period. + +block period +name first +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name last +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name frequency +type integer +shape +tagged true +in_record true +reader urword +longname +description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name steps +type integer +shape ($ 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'. + +block period +name shutoffrecord +type record shut_off minrate maxrate +shape +tagged +in_record true +reader urword +longname +description + +block period +name shut_off +type keyword +shape +in_record true +reader urword +longname shut off well +description keyword for activating well shut off capability. Subsequent values define the minimum and maximum pumping rate that a well must exceed to shutoff or reactivate a well, respectively, during a stress period. SHUT\_OFF is only applied to injection wells (RATE$<0$) and if HEAD\_LIMIT is specified (not set to `OFF'). If HEAD\_LIMIT is specified, SHUT\_OFF can be deactivated by specifying a minimum value equal to zero. The SHUT\_OFF option is based on the SHUT\_OFF functionality available in the MNW2~\citep{konikow2009} package for MODFLOW-2005. The SHUT\_OFF option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE\_SCALING option instead of the SHUT\_OFF option is recommended. By default, SHUT\_OFF is not used. + +block period +name minrate +type double precision +shape +tagged false +in_record true +reader urword +longname minimum shutoff rate +description is the minimum rate that a well must exceed to shutoff a well during a stress period. The well will shut down during a time step if the flow rate to the well from the aquifer is less than MINRATE. If a well is shut down during a time step, reactivation of the well cannot occur until the next time step to reduce oscillations. MINRATE must be less than maxrate. + +block period +name maxrate +type double precision +shape +tagged false +in_record true +reader urword +longname maximum shutoff rate +description is the maximum rate that a well must exceed to reactivate a well during a stress period. The well will reactivate during a timestep if the well was shutdown during the previous time step and the flow rate to the well from the aquifer exceeds maxrate. Reactivation of the well cannot occur until the next time step if a well is shutdown to reduce oscillations. maxrate must be greater than MINRATE. + +block period +name rate_scalingrecord +type record rate_scaling pump_elevation scaling_length +shape +tagged +in_record true +reader urword +longname +description + +block period +name rate_scaling +type keyword +shape +in_record true +reader urword +longname rate scaling +description activate rate scaling. If RATE\_SCALING is specified, both PUMP\_ELEVATION and SCALING\_LENGTH must be specified. RATE\_SCALING cannot be used with HEAD\_LIMIT. RATE\_SCALING can be used for extraction or injection wells. For extraction wells, the extraction rate will start to decrease once the head in the well lowers to a level equal to the pump elevation plus the scaling length. If the head in the well drops below the pump elevation, then the extraction rate is calculated to be zero. For an injection well, the injection rate will begin to decrease once the head in the well rises above the specified pump elevation. If the head in the well rises above the pump elevation plus the scaling length, then the injection rate will be set to zero. + +block period +name pump_elevation +type double precision +shape +tagged false +in_record true +reader urword +longname pump elevation +description is the elevation of the multi-aquifer well pump (PUMP\_ELEVATION). PUMP\_ELEVATION should not be less than the bottom elevation (BOTTOM) of the multi-aquifer well. + +block period +name scaling_length +type double precision +shape +tagged false +in_record true +reader urword +longname +description height above the pump elevation (SCALING\_LENGTH). If the simulated well head is below this elevation (pump elevation plus the scaling length), then the pumping rate is reduced. + +block period +name auxiliaryrecord +type record auxiliary auxname auxval +shape +tagged +in_record true +reader urword +longname +description + +block period +name auxiliary +type keyword +shape +in_record true +reader urword +longname +description keyword for specifying auxiliary variable. + +block period +name auxname +type string +shape +tagged false +in_record true +reader urword +longname +description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. + +block period +name auxval +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname auxiliary variable value +description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwf-mvr.dfn b/autotest/autotest/temp/dfn/gwf-mvr.dfn new file mode 100644 index 00000000..62adad3b --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-mvr.dfn @@ -0,0 +1,265 @@ +# --------------------- gwf mvr options --------------------- +# flopy subpackage mvr_filerecord mvr perioddata perioddata +# flopy parent_name_type parent_model_or_package MFModel/MFPackage + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'MVR'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'MVR'} + +block options +name modelnames +type keyword +reader urword +optional true +longname precede all package names with model names +description keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +# --------------------- gwf mvr dimensions --------------------- + +block dimensions +name maxmvr +type integer +reader urword +optional false +longname maximum number of movers +description integer value specifying the maximum number of water mover entries that will specified for any stress period. + +block dimensions +name maxpackages +type integer +reader urword +optional false +longname number of packages to be used with the mover +description integer value specifying the number of unique packages that are included in this water mover input file. + + +# --------------------- gwf mvr packages --------------------- + +block packages +name packages +type recarray mname pname +reader urword +shape (npackages) +optional false +longname +description + +block packages +name mname +type string +reader urword +shape +tagged false +in_record true +optional true +longname +description name of model containing the package. Model names are assigned by the user in the simulation name file. + +block packages +name pname +type string +reader urword +shape +tagged false +in_record true +optional false +longname +description is the name of a package that may be included in a subsequent stress period block. The package name is assigned in the name file for the GWF Model. Package names are optionally provided in the name file. If they are not provided by the user, then packages are assigned a default value, which is the package acronym followed by a hyphen and the package number. For example, the first Drain Package is named DRN-1. The second Drain Package is named DRN-2, and so forth. + + +# --------------------- gwf mvr period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name perioddata +type recarray mname1 pname1 id1 mname2 pname2 id2 mvrtype value +shape (maxbound) +reader urword +longname +description + +block period +name mname1 +type string +reader urword +shape +tagged false +in_record true +optional true +longname +description name of model containing the package, PNAME1. + +block period +name pname1 +type string +shape +tagged false +in_record true +reader urword +longname provider package name +description is the package name for the provider. The package PNAME1 must be designated to provide water through the MVR Package by specifying the keyword ``MOVER'' in its OPTIONS block. + +block period +name id1 +type integer +shape +tagged false +in_record true +reader urword +longname provider reach +description is the identifier for the provider. For the standard boundary packages, the provider identifier is the number of the boundary as it is listed in the package input file. (Note that the order of these boundaries may change by stress period, which must be accounted for in the Mover Package.) So the first well has an identifier of one. The second is two, and so forth. For the advanced packages, the identifier is the reach number (SFR Package), well number (MAW Package), or UZF cell number. For the Lake Package, ID1 is the lake outlet number. Thus, outflows from a single lake can be routed to different streams, for example. +numeric_index true + +block period +name mname2 +type string +reader urword +shape +tagged false +in_record true +optional true +longname +description name of model containing the package, PNAME2. + +block period +name pname2 +type string +shape +tagged false +in_record true +reader urword +longname receiver package name +description is the package name for the receiver. The package PNAME2 must be designated to receive water from the MVR Package by specifying the keyword ``MOVER'' in its OPTIONS block. + +block period +name id2 +type integer +shape +tagged false +in_record true +reader urword +longname receiver reach +description is the identifier for the receiver. The receiver identifier is the reach number (SFR Package), Lake number (LAK Package), well number (MAW Package), or UZF cell number. +numeric_index true + +block period +name mvrtype +type string +shape +tagged false +in_record true +reader urword +longname mover type +description is the character string signifying the method for determining how much water will be moved. Supported values are ``FACTOR'' ``EXCESS'' ``THRESHOLD'' and ``UPTO''. These four options determine how the receiver flow rate, $Q_R$, is calculated. These options mirror the options defined for the cprior variable in the SFR package, with the term ``FACTOR'' being functionally equivalent to the ``FRACTION'' option for cprior. + +block period +name value +type double precision +shape +tagged false +in_record true +reader urword +longname mover value +description is the value to be used in the equation for calculating the amount of water to move. For the ``FACTOR'' option, VALUE is the $\alpha$ factor. For the remaining options, VALUE is the specified flow rate, $Q_S$. + diff --git a/autotest/autotest/temp/dfn/gwf-nam.dfn b/autotest/autotest/temp/dfn/gwf-nam.dfn new file mode 100644 index 00000000..2718264d --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-nam.dfn @@ -0,0 +1,226 @@ +# --------------------- gwf nam options --------------------- + +block options +name list +type string +reader urword +optional true +preserve_case true +longname name of listing file +description is name of the listing file to create for this GWF model. If not specified, then the name of the list file will be the basename of the GWF model name file and the '.lst' extension. For example, if the GWF name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'all model stress package'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'all model package'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save flows for all packages to budget file +description REPLACE save_flows {'{#1}': 'all model package'} + +block options +name newtonoptions +type record newton under_relaxation +reader urword +optional true +longname newton keyword and options +description none + +block options +name newton +in_record true +type keyword +reader urword +longname keyword to activate Newton-Raphson formulation +description keyword that activates the Newton-Raphson formulation for groundwater flow between connected, convertible groundwater cells and stress packages that support calculation of Newton-Raphson terms for groundwater exchanges. Cells will not dry when this option is used. By default, the Newton-Raphson formulation is not applied. + +block options +name under_relaxation +in_record true +type keyword +reader urword +optional true +longname keyword to activate Newton-Raphson UNDER_RELAXATION option +description keyword that indicates whether the groundwater head in a cell will be under-relaxed when water levels fall below the bottom of the model below any given cell. By default, Newton-Raphson UNDER\_RELAXATION is not applied. + +block options +name nc_mesh2d_filerecord +type record netcdf_mesh2d fileout ncmesh2dfile +shape +reader urword +tagged true +optional true +longname +description NetCDF layered mesh fileout record. +mf6internal ncmesh2drec + +block options +name netcdf_mesh2d +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a layered mesh NetCDF file. +extended true + +block options +name nc_structured_filerecord +type record netcdf_structured fileout ncstructfile +shape +reader urword +tagged true +optional true +longname +description NetCDF structured fileout record. +mf6internal ncstructrec + +block options +name netcdf_structured +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a structured NetCDF file. +mf6internal netcdf_struct +extended true + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name ncmesh2dfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF ugrid layered mesh output file. +extended true + +block options +name ncstructfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF structured output file. +extended true + +block options +name nc_filerecord +type record netcdf filein netcdf_filename +reader urword +tagged true +optional true +longname +description NetCDF filerecord + +block options +name netcdf +type keyword +in_record true +reader urword +tagged true +optional false +longname netcdf keyword +description keyword to specify that record corresponds to a NetCDF input file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name netcdf_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname netcdf input filename +description defines a NetCDF input file. +mf6internal netcdf_fname +extended true + +# --------------------- gwf nam packages --------------------- + +block packages +name packages +type recarray ftype fname pname +reader urword +optional false +longname package list +description + +block packages +name ftype +in_record true +type string +tagged false +reader urword +longname package type +description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwf}. Ftype may be entered in any combination of uppercase and lowercase. + +block packages +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. + +block packages +name pname +in_record true +type string +tagged false +reader urword +optional true +longname user name for package +description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWF Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. + diff --git a/autotest/autotest/temp/dfn/gwf-npf.dfn b/autotest/autotest/temp/dfn/gwf-npf.dfn new file mode 100644 index 00000000..3626b812 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-npf.dfn @@ -0,0 +1,375 @@ +# --------------------- gwf npf options --------------------- +# mf6 subpackage utl-tvk + +block options +name save_flows +type keyword +reader urword +optional true +longname keyword to save NPF flows +description keyword to indicate that budget flow terms will be written to the file specified with ``BUDGET SAVE FILE'' in Output Control. +mf6internal ipakcb + +block options +name print_flows +type keyword +reader urword +optional true +longname keyword to print NPF flows to listing file +description keyword to indicate that calculated flows between cells will be printed to the listing file for every stress period time step in which ``BUDGET PRINT'' is specified in Output Control. If there is no Output Control option and ``PRINT\_FLOWS'' is specified, then flow rates are printed for the last time step of each stress period. This option can produce extremely large list files because all cell-by-cell flows are printed. It should only be used with the NPF Package for models that have a small number of cells. +mf6internal iprflow + +block options +name alternative_cell_averaging +type string +valid logarithmic amt-lmk amt-hmk +reader urword +optional true +longname conductance weighting option +description is a text keyword to indicate that an alternative method will be used for calculating the conductance for horizontal cell connections. The text value for ALTERNATIVE\_CELL\_AVERAGING can be ``LOGARITHMIC'', ``AMT-LMK'', or ``AMT-HMK''. ``AMT-LMK'' signifies that the conductance will be calculated using arithmetic-mean thickness and logarithmic-mean hydraulic conductivity. ``AMT-HMK'' signifies that the conductance will be calculated using arithmetic-mean thickness and harmonic-mean hydraulic conductivity. If the user does not specify a value for ALTERNATIVE\_CELL\_AVERAGING, then the harmonic-mean method will be used. This option cannot be used if the XT3D option is invoked. The AMT-HMK ALTERNATIVE\_CELL\_AVERAGING option, in combination with the DRY\_CELL\_SATURATION option, can be used to calculate the same horizontal conductance as MODFLOW-USG when upstream weighting is used (LAYCON=4). +mf6internal cellavg + +block options +name thickstrt +type keyword +reader urword +optional true +longname keyword to activate THICKSTRT option +description indicates that cells having a negative ICELLTYPE are confined, and their cell thickness for conductance calculations will be computed as STRT-BOT rather than TOP-BOT. This option should be used with caution as it only affects conductance calculations in the NPF Package. +mf6internal ithickstrt + +block options +name cvoptions +type record variablecv dewatered +reader urword +optional true +longname vertical conductance options +description none + +block options +name variablecv +in_record true +type keyword +reader urword +longname keyword to activate VARIABLECV option +description keyword to indicate that the vertical conductance will be calculated using the saturated thickness and properties of the overlying cell and the thickness and properties of the underlying cell. If the DEWATERED keyword is also specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. If these keywords are not specified, then the default condition is to calculate the vertical conductance at the start of the simulation using the initial head and the cell properties. The vertical conductance remains constant for the entire simulation. +mf6internal ivarcv + +block options +name dewatered +in_record true +type keyword +reader urword +optional true +longname keyword to activate DEWATERED option +description If the DEWATERED keyword is specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. +mf6internal idewatcv + +block options +name perched +type keyword +reader urword +optional true +longname keyword to activate PERCHED option +description keyword to indicate that when a cell is overlying a dewatered convertible cell, the head difference used in Darcy's Law is equal to the head in the overlying cell minus the bottom elevation of the overlying cell. If not specified, then the default is to use the head difference between the two cells. +mf6internal iperched + +block options +name rewet_record +type record rewet wetfct iwetit ihdwet +reader urword +optional true +longname +description + +block options +name rewet +type keyword +in_record true +reader urword +optional false +longname keyword to activate rewetting +description activates model rewetting. Rewetting is off by default. +mf6internal irewet + +block options +name wetfct +type double precision +in_record true +reader urword +optional false +longname wetting factor to use for rewetting +description is a keyword and factor that is included in the calculation of the head that is initially established at a cell when that cell is converted from dry to wet. + +block options +name iwetit +type integer +in_record true +reader urword +optional false +longname interval to use for rewetting +description is a keyword and iteration interval for attempting to wet cells. Wetting is attempted every IWETIT iteration. This applies to outer iterations and not inner iterations. If IWETIT is specified as zero or less, then the value is changed to 1. + +block options +name ihdwet +type integer +in_record true +reader urword +optional false +longname flag to determine wetting equation +description is a keyword and integer flag that determines which equation is used to define the initial head at cells that become wet. If IHDWET is 0, h = BOT + WETFCT (hm - BOT). If IHDWET is not 0, h = BOT + WETFCT (THRESH). + +block options +name xt3doptions +type record xt3d rhs +reader urword +optional true +longname keyword to activate XT3D +description none + +block options +name xt3d +in_record true +type keyword +reader urword +longname keyword to activate XT3D +description keyword indicating that the XT3D formulation will be used. If the RHS keyword is also included, then the XT3D additional terms will be added to the right-hand side. If the RHS keyword is excluded, then the XT3D terms will be put into the coefficient matrix. Use of XT3D will substantially increase the computational effort, but will result in improved accuracy for anisotropic conductivity fields and for unstructured grids in which the CVFD requirement is violated. XT3D requires additional information about the shapes of grid cells. If XT3D is active and the DISU Package is used, then the user will need to provide in the DISU Package the angldegx array in the CONNECTIONDATA block and the VERTICES and CELL2D blocks. +mf6internal ixt3d + +block options +name rhs +in_record true +type keyword +reader urword +optional true +longname keyword to XT3D on right hand side +description If the RHS keyword is also included, then the XT3D additional terms will be added to the right-hand side. If the RHS keyword is excluded, then the XT3D terms will be put into the coefficient matrix. +mf6internal ixt3drhs + +block options +name highest_cell_saturation +type keyword +reader urword +optional true +longname keyword to activate HIGHEST_CELL_SATURATION option +description keyword indicating that the maximum cell bottom will be used to calculate the saturation used to calculate the horizontal conductance between cells. This option is intended to prevent flow from leaving a dry cell and is based on~\cite{painter2008robust}. This option is only applied when the Newton-Raphson formulation is used, A warning will be issued if this option is specified and the Newton-Raphson formulation is not specified in the GWF name file. This option, in combination with the AMT-HMK ALTERNATIVE\_CELL\_AVERAGING option, can be used to calculate the same horizontal conductance as MODFLOW-USG when upstream weighting is used (LAYCON=4). +mf6internal ihighcellsat + + +block options +name save_specific_discharge +type keyword +reader urword +optional true +longname keyword to save specific discharge +description keyword to indicate that x, y, and z components of specific discharge will be calculated at cell centers and written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. If this option is activated, then additional information may be required in the discretization packages and the GWF Exchange package (if GWF models are coupled). Specifically, ANGLDEGX must be specified in the CONNECTIONDATA block of the DISU Package; ANGLDEGX must also be specified for the GWF Exchange as an auxiliary variable. +mf6internal isavspdis + +block options +name save_saturation +type keyword +reader urword +optional true +longname keyword to save saturation +description keyword to indicate that cell saturation will be written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. Saturation will be saved to the budget file as an auxiliary variable saved with the DATA-SAT text label. Saturation is a cell variable that ranges from zero to one and can be used by post processing programs to determine how much of a cell volume is saturated. If ICELLTYPE is 0, then saturation is always one. +mf6internal isavsat + +block options +name k22overk +type keyword +reader urword +optional true +longname keyword to indicate that specified K22 is a ratio +description keyword to indicate that specified K22 is a ratio of K22 divided by K. If this option is specified, then the K22 array entered in the NPF Package will be multiplied by K after being read. +mf6internal ik22overk + +block options +name k33overk +type keyword +reader urword +optional true +longname keyword to indicate that specified K33 is a ratio +description keyword to indicate that specified K33 is a ratio of K33 divided by K. If this option is specified, then the K33 array entered in the NPF Package will be multiplied by K after being read. +mf6internal ik33overk + +block options +name tvk_filerecord +type record tvk6 filein tvk6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name tvk6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname tvk keyword +description keyword to specify that record corresponds to a time-varying hydraulic conductivity (TVK) file. The behavior of TVK and a description of the input file is provided separately. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name tvk6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of TVK information +description defines a time-varying hydraulic conductivity (TVK) input file. Records in the TVK file can be used to change hydraulic conductivity properties at specified times or stress periods. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# dev options + +block options +name dev_no_newton +type keyword +reader urword +optional true +longname turn off Newton for unconfined cells +description turn off Newton for unconfined cells +mf6internal inewton + +block options +name dev_omega +type double precision +reader urword +optional true +longname set saturation omega value +description set saturation omega value +mf6internal satomega + +# --------------------- gwf npf griddata --------------------- + +block griddata +name icelltype +type integer +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional +longname confined or convertible indicator +description flag for each cell that specifies how saturated thickness is treated. 0 means saturated thickness is held constant; $>$0 means saturated thickness varies with computed head when head is below the cell top; $<$0 means saturated thickness varies with computed head unless the THICKSTRT option is in effect. When THICKSTRT is in effect, a negative value for ICELLTYPE indicates that the saturated thickness value used in conductance calculations in the NPF Package will be computed as STRT-BOT and held constant. If the THICKSTRT option is not in effect, then negative values provided by the user for ICELLTYPE are automatically reassigned by the program to a value of one. +default_value 0 + +block griddata +name k +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional +longname hydraulic conductivity (L/T) +description is the hydraulic conductivity. For the common case in which the user would like to specify the horizontal hydraulic conductivity and the vertical hydraulic conductivity, then K should be assigned as the horizontal hydraulic conductivity, K33 should be assigned as the vertical hydraulic conductivity, and K22 and the three rotation angles should not be specified. When more sophisticated anisotropy is required, then K corresponds to the K11 hydraulic conductivity axis. All included cells (IDOMAIN $>$ 0) must have a K value greater than zero. +default_value 1.0 + +block griddata +name k22 +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname hydraulic conductivity of second ellipsoid axis +description is the hydraulic conductivity of the second ellipsoid axis (or the ratio of K22/K if the K22OVERK option is specified); for an unrotated case this is the hydraulic conductivity in the y direction. If K22 is not included in the GRIDDATA block, then K22 is set equal to K. For a regular MODFLOW grid (DIS Package is used) in which no rotation angles are specified, K22 is the hydraulic conductivity along columns in the y direction. For an unstructured DISU grid, the user must assign principal x and y axes and provide the angle for each cell face relative to the assigned x direction. All included cells (IDOMAIN $>$ 0) must have a K22 value greater than zero. + +block griddata +name k33 +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname hydraulic conductivity of third ellipsoid axis (L/T) +description is the hydraulic conductivity of the third ellipsoid axis (or the ratio of K33/K if the K33OVERK option is specified); for an unrotated case, this is the vertical hydraulic conductivity. When anisotropy is applied, K33 corresponds to the K33 tensor component. All included cells (IDOMAIN $>$ 0) must have a K33 value greater than zero. + +block griddata +name angle1 +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname first anisotropy rotation angle (degrees) +description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the first of three sequential rotations of the hydraulic conductivity ellipsoid. With the K11, K22, and K33 axes of the ellipsoid initially aligned with the x, y, and z coordinate axes, respectively, ANGLE1 rotates the ellipsoid about its K33 axis (within the x - y plane). A positive value represents counter-clockwise rotation when viewed from any point on the positive K33 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K11 axis lies within the x - z plane. If ANGLE1 is not specified, default values of zero are assigned to ANGLE1, ANGLE2, and ANGLE3, in which case the K11, K22, and K33 axes are aligned with the x, y, and z axes, respectively. + +block griddata +name angle2 +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname second anisotropy rotation angle (degrees) +description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the second of three sequential rotations of the hydraulic conductivity ellipsoid. Following the rotation by ANGLE1 described above, ANGLE2 rotates the ellipsoid about its K22 axis (out of the x - y plane). An array can be specified for ANGLE2 only if ANGLE1 is also specified. A positive value of ANGLE2 represents clockwise rotation when viewed from any point on the positive K22 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K11 axis lies within the x - y plane. If ANGLE2 is not specified, default values of zero are assigned to ANGLE2 and ANGLE3; connections that are not user-designated as vertical are assumed to be strictly horizontal (that is, to have no z component to their orientation); and connection lengths are based on horizontal distances. + +block griddata +name angle3 +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname third anisotropy rotation angle (degrees) +description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the third of three sequential rotations of the hydraulic conductivity ellipsoid. Following the rotations by ANGLE1 and ANGLE2 described above, ANGLE3 rotates the ellipsoid about its K11 axis. An array can be specified for ANGLE3 only if ANGLE1 and ANGLE2 are also specified. An array must be specified for ANGLE3 if ANGLE2 is specified. A positive value of ANGLE3 represents clockwise rotation when viewed from any point on the positive K11 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K22 axis lies within the x - y plane. + +block griddata +name wetdry +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional true +longname wetdry threshold and factor +description is a combination of the wetting threshold and a flag to indicate which neighboring cells can cause a cell to become wet. If WETDRY $<$ 0, only a cell below a dry cell can cause the cell to become wet. If WETDRY $>$ 0, the cell below a dry cell and horizontally adjacent cells can cause a cell to become wet. If WETDRY is 0, the cell cannot be wetted. The absolute value of WETDRY is the wetting threshold. When the sum of BOT and the absolute value of WETDRY at a dry cell is equaled or exceeded by the head at an adjacent cell, the cell is wetted. WETDRY must be specified if ``REWET'' is specified in the OPTIONS block. If ``REWET'' is not specified in the options block, then WETDRY can be entered, and memory will be allocated for it, even though it is not used. diff --git a/autotest/autotest/temp/dfn/gwf-oc.dfn b/autotest/autotest/temp/dfn/gwf-oc.dfn new file mode 100644 index 00000000..f4eb2803 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-oc.dfn @@ -0,0 +1,317 @@ +# --------------------- gwf oc options --------------------- + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +mf6internal budfilerec +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +mf6internal budcsvfilerec +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name head_filerecord +type record head fileout headfile +shape +reader urword +tagged true +optional true +mf6internal headfilerec +longname +description + +block options +name head +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to head. + +block options +name headfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write head information. + +block options +name headprintrecord +type record head print_format formatrecord +shape +reader urword +optional true +mf6internal headprintrec +longname +description + +block options +name print_format +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to indicate that a print format follows +description keyword to specify format for printing to the listing file. + +block options +name formatrecord +type record columns width digits format +shape +in_record true +reader urword +tagged +optional false +longname +description + +block options +name columns +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of columns +description number of columns for writing data. + +block options +name width +type integer +shape +in_record true +reader urword +tagged true +optional +longname width for each number +description width for writing each number. + +block options +name digits +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of digits +description number of digits to use for writing a number. + +block options +name format +type string +shape +in_record true +reader urword +tagged false +optional false +longname write format +description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. + + +# --------------------- gwf oc period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name saverecord +type record save rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name save +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be saved this stress period. + +block period +name printrecord +type record print rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name print +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be printed this stress period. + +block period +name rtype +type string +shape +in_record true +reader urword +tagged false +optional false +longname record type +description type of information to save or print. Can be BUDGET or HEAD. + +block period +name ocsetting +type keystring all first last frequency steps +shape +tagged false +in_record true +reader urword +longname +description specifies the steps for which the data will be saved. + +block period +name all +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for all time steps in period. + +block period +name first +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name last +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name frequency +type integer +shape +tagged true +in_record true +reader urword +longname +description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name steps +type integer +shape (0) when the cell is under confined conditions (head greater than or equal to the top of the cell). This option has no effect on cells that are marked as being always confined (ICONVERT=0). This option is identical to the approach used to calculate storage changes under confined conditions in MODFLOW-2005. + +block options +name tvs_filerecord +type record tvs6 filein tvs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name tvs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname tvs keyword +description keyword to specify that record corresponds to a time-varying storage (TVS) file. The behavior of TVS and a description of the input file is provided separately. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name tvs6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of TVS information +description defines a time-varying storage (TVS) input file. Records in the TVS file can be used to change specific storage and specific yield properties at specified times or stress periods. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input grid arrays, which already support the layered keyword, should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# dev options +block options +name dev_original_specific_storage +type keyword +reader urword +optional true +longname development option for original specific storage +description flag indicating the original storage specific storage formulation should be used +mf6internal iorig_ss + +block options +name dev_oldstorageformulation +type keyword +reader urword +optional true +longname development option flag for old storage formulation +description development option flag for old storage formulation +mf6internal iconf_ss + +# --------------------- gwf sto griddata --------------------- + +block griddata +name iconvert +type integer +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional false +longname convertible indicator +description is a flag for each cell that specifies whether or not a cell is convertible for the storage calculation. 0 indicates confined storage is used. $>$0 indicates confined storage is used when head is above cell top and a mixed formulation of unconfined and confined storage is used when head is below cell top. +default_value 0 + +block griddata +name ss +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional false +longname specific storage +description is specific storage (or the storage coefficient if STORAGECOEFFICIENT is specified as an option). Specific storage values must be greater than or equal to 0. If the CSUB Package is included in the GWF model, specific storage must be zero for every cell. +default_value 1.e-5 + +block griddata +name sy +type double precision +shape (nodes) +valid +reader readarray +layered true +netcdf true +optional false +longname specific yield +description is specific yield. Specific yield values must be greater than or equal to 0. Specific yield does not have to be specified if there are no convertible cells (ICONVERT=0 in every cell). +default_value 0.15 + + +# --------------------- gwf sto period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name steady-state +type keyword +shape +valid +reader urword +optional true +longname steady state indicator +description keyword to indicate that stress period IPER is steady-state. Steady-state conditions will apply until the TRANSIENT keyword is specified in a subsequent BEGIN PERIOD block. If the CSUB Package is included in the GWF model, only the first and last stress period can be steady-state. +mf6internal steady_state + +block period +name transient +type keyword +shape +valid +reader urword +optional true +longname transient indicator +description keyword to indicate that stress period IPER is transient. Transient conditions will apply until the STEADY-STATE keyword is specified in a subsequent BEGIN PERIOD block. diff --git a/autotest/autotest/temp/dfn/gwf-uzf.dfn b/autotest/autotest/temp/dfn/gwf-uzf.dfn new file mode 100644 index 00000000..76ba773b --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-uzf.dfn @@ -0,0 +1,611 @@ +# --------------------- gwf uzf options --------------------- +# flopy multi-package +# package-type advanced-stress-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'GWF cell area used by UZF cell'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'UZF'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'UZF'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'UZF'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'UZF'} + +block options +name wc_filerecord +type record water_content fileout wcfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name water_content +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname water_content keyword +description keyword to specify that record corresponds to unsaturated zone water contents. + +block options +name wcfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write water content information. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +preserve_case true +type string +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name package_convergence_filerecord +type record package_convergence fileout package_convergence_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name package_convergence +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname package_convergence keyword +description keyword to specify that record corresponds to the package convergence comma spaced values file. + +block options +name package_convergence_filename +type string +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma spaced values output file to write package convergence information. + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'UZF', '{#2}': '\\ref{table:gwf-obstypetable}'} + +block options +name mover +type keyword +tagged true +reader urword +optional true +longname +description REPLACE mover {'{#1}': 'UZF'} + +block options +name simulate_et +type keyword +tagged true +reader urword +optional true +longname +description keyword specifying that ET in the unsaturated (UZF) and saturated zones (GWF) will be simulated. ET can be simulated in the UZF cell and not the GWF cell by omitting keywords LINEAR\_GWET and SQUARE\_GWET. + +block options +name linear_gwet +type keyword +tagged true +reader urword +optional true +longname use linear evapotranspiration +description keyword specifying that groundwater ET will be simulated using the original ET formulation of MODFLOW-2005. + +block options +name square_gwet +type keyword +tagged true +reader urword +optional true +longname use square evapotranspiration +description keyword specifying that groundwater ET will be simulated by assuming a constant ET rate for groundwater levels between land surface (TOP) and land surface minus the ET extinction depth (TOP-EXTDP). Groundwater ET is smoothly reduced from the PET rate to zero over a nominal interval at TOP-EXTDP. + +block options +name simulate_gwseep +type keyword +tagged true +reader urword +optional true +deprecated 6.5.0 +longname activate seepage +description keyword specifying that groundwater discharge (GWSEEP) to land surface will be simulated. Groundwater discharge is nonzero when groundwater head is greater than land surface. This option is no longer recommended; a better approach is to use the Drain Package with discharge scaling as a way to handle seepage to land surface. The Drain Package with discharge scaling is described in Chapter 3 of the Supplemental Technical Information. + +block options +name unsat_etwc +type keyword +tagged true +reader urword +optional true +longname use PET for theta greater than extwc +description keyword specifying that ET in the unsaturated zone will be simulated as a function of the specified PET rate while the water content (THETA) is greater than the ET extinction water content (EXTWC). + +block options +name unsat_etae +type keyword +tagged true +reader urword +optional true +longname use root potential +description keyword specifying that ET in the unsaturated zone will be simulated using a capillary pressure based formulation. Capillary pressure is calculated using the Brooks-Corey retention function. + + +# --------------------- gwf uzf dimensions --------------------- + +block dimensions +name nuzfcells +type integer +reader urword +optional false +longname number of UZF cells +description is the number of UZF cells. More than one UZF cell can be assigned to a GWF cell; however, only one GWF cell can be assigned to a single UZF cell. If more than one UZF cell is assigned to a GWF cell, then an auxiliary variable should be used to reduce the surface area of the UZF cell with the AUXMULTNAME option. + +block dimensions +name ntrailwaves +type integer +reader urword +optional false +longname number of trailing waves +description is the number of trailing waves. A recommended value of 7 can be used for NTRAILWAVES. This value can be increased to lower mass balance error in the unsaturated zone. +default_value 7 + +block dimensions +name nwavesets +type integer +reader urword +optional false +longname number of wave sets +description is the number of wave sets. A recommended value of 40 can be used for NWAVESETS. This value can be increased if more waves are required to resolve variations in water content within the unsaturated zone. +default_value 40 + +# --------------------- gwf uzf packagedata --------------------- + +block packagedata +name packagedata +type recarray ifno cellid landflag ivertcon surfdep vks thtr thts thti eps boundname +shape (nuzfcells) +reader urword +longname +description + +block packagedata +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname uzf id number for this entry +description integer value that defines the feature (UZF object) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NUZFCELLS. UZF information must be specified for every UZF cell or the program will terminate with an error. The program will also terminate with an error if information for a UZF cell is specified more than once. +numeric_index true + +block packagedata +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block packagedata +name landflag +type integer +shape +tagged false +in_record true +reader urword +longname land flag +description integer value set to one for land surface cells indicating that boundary conditions can be applied and data can be specified in the PERIOD block. A value of 0 specifies a non-land surface cell. + +block packagedata +name ivertcon +type integer +shape +tagged false +in_record true +reader urword +longname vertical connection flag +description integer value set to specify underlying UZF cell that receives water flowing to bottom of cell. If unsaturated zone flow reaches the water table before the cell bottom, then water is added to the GWF cell instead of flowing to the underlying UZF cell. A value of 0 indicates the UZF cell is not connected to an underlying UZF cell. +numeric_index true + +block packagedata +name surfdep +type double precision +shape +tagged false +in_record true +reader urword +longname surface depression depth +description is the surface depression depth of the UZF cell. + +block packagedata +name vks +type double precision +shape +tagged false +in_record true +reader urword +longname vertical saturated hydraulic conductivity +description is the saturated vertical hydraulic conductivity of the UZF cell. This value is used with the Brooks-Corey function and the simulated water content to calculate the partially saturated hydraulic conductivity. + +block packagedata +name thtr +type double precision +shape +tagged false +in_record true +reader urword +longname residual water content +description is the residual (irreducible) water content of the UZF cell. This residual water is not available to plants and will not drain into underlying aquifer cells. + +block packagedata +name thts +type double precision +shape +tagged false +in_record true +reader urword +longname saturated water content +description is the saturated water content of the UZF cell. The values for saturated and residual water content should be set in a manner that is consistent with the specific yield value specified in the Storage Package. The saturated water content must be greater than the residual content. + +block packagedata +name thti +type double precision +shape +tagged false +in_record true +reader urword +longname initial water content +description is the initial water content of the UZF cell. The value must be greater than or equal to the residual water content and less than or equal to the saturated water content. + +block packagedata +name eps +type double precision +shape +tagged false +in_record true +reader urword +longname Brooks-Corey exponent +description is the exponent used in the Brooks-Corey function. The Brooks-Corey function is used by UZF to calculated hydraulic conductivity under partially saturated conditions as a function of water content and the user-specified saturated hydraulic conductivity. + +block packagedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname well name +description REPLACE boundname {'{#1}': 'UZF cell'} + + +# --------------------- gwf uzf period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name perioddata +type recarray ifno finf pet extdp extwc ha hroot rootact aux +shape +reader urword +longname +description + +block period +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname UZF id number +description integer value that defines the feature (UZF object) number associated with the specified PERIOD data on the line. +numeric_index true + +block period +name finf +type string +shape +tagged false +in_record true +time_series true +reader urword +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. + +block period +name pet +type string +shape +tagged false +in_record true +reader urword +time_series true +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. + +block period +name extdp +type string +shape +tagged false +in_record true +reader urword +time_series true +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. + +block period +name extwc +type string +shape +tagged false +in_record true +reader urword +time_series true +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. + +block period +name ha +type string +shape +tagged false +in_record true +time_series true +reader urword +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. + +block period +name hroot +type string +shape +tagged false +in_record true +reader urword +time_series true +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. + +block period +name rootact +type string +shape +tagged false +in_record true +reader urword +time_series true +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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +time_series true +optional true +longname auxiliary variables +description REPLACE aux {'{#1}': 'UZF'} diff --git a/autotest/autotest/temp/dfn/gwf-vsc.dfn b/autotest/autotest/temp/dfn/gwf-vsc.dfn new file mode 100644 index 00000000..9c0f93e6 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-vsc.dfn @@ -0,0 +1,177 @@ +# --------------------- gwf vsc options --------------------- + +block options +name viscref +type double precision +reader urword +optional true +longname reference viscosity +description fluid reference viscosity used in the equation of state. This value is set to 1.0 if not specified as an option. +default_value 1.0 + +block options +name temperature_species_name +type string +shape +reader urword +optional true +mf6internal temp_specname +longname auxspeciesname that corresponds to temperature +description string used to identify the auxspeciesname in PACKAGEDATA that corresponds to the temperature species. There can be only one occurrence of this temperature species name in the PACKAGEDATA block or the program will terminate with an error. This value has no effect if viscosity does not depend on temperature. + +block options +name thermal_formulation +type string +shape +reader urword +optional true +valid linear nonlinear +mf6internal thermal_form +longname keyword to specify viscosity formulation for the temperature species +description may be used for specifying which viscosity formulation to use for the temperature species. Can be either LINEAR or NONLINEAR. The LINEAR viscosity formulation is the default. + +block options +name thermal_a2 +type double precision +reader urword +optional true +longname coefficient used in nonlinear viscosity function +description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A2 is not specified, a default value of 10.0 is assigned (Voss, 1984). +default_value 10. + +block options +name thermal_a3 +type double precision +reader urword +optional true +longname coefficient used in nonlinear viscosity function +description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A3 is not specified, a default value of 248.37 is assigned (Voss, 1984). +default_value 248.37 + +block options +name thermal_a4 +type double precision +reader urword +optional true +longname coefficient used in nonlinear viscosity function +description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A4 is not specified, a default value of 133.15 is assigned (Voss, 1984). +default_value 133.15 + +block options +name viscosity_filerecord +type record viscosity fileout viscosityfile +shape +reader urword +tagged true +optional true +mf6internal viscosity_fr +longname +description + +block options +name viscosity +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname viscosity keyword +description keyword to specify that record corresponds to viscosity. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name viscosityfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write viscosity information. The viscosity file has the same format as the head file. Viscosity values will be written to the viscosity file whenever heads are written to the binary head file. The settings for controlling head output are contained in the Output Control option. + + +# --------------------- gwf vsc dimensions --------------------- + +block dimensions +name nviscspecies +type integer +reader urword +optional false +longname number of species used in viscosity equation of state +description number of species used in the viscosity equation of state. If either concentrations or temperature (or both) are used to update viscosity then nviscspecies needs to be at least one. + + +# --------------------- gwf vsc packagedata --------------------- + +block packagedata +name packagedata +type recarray iviscspec dviscdc cviscref modelname auxspeciesname +shape (nviscspecies) +reader urword +longname +description + +block packagedata +name iviscspec +type integer +shape +tagged false +in_record true +reader urword +longname species number for this entry +description integer value that defines the species number associated with the specified PACKAGEDATA data entered on each line. IVISCSPECIES must be greater than zero and less than or equal to NVISCSPECIES. Information must be specified for each of the NVISCSPECIES species or the program will terminate with an error. The program will also terminate with an error if information for a species is specified more than once. +numeric_index true + +block packagedata +name dviscdc +type double precision +shape +tagged false +in_record true +reader urword +longname slope of the line that defines the linear relationship between viscosity and temperature or between viscosity and concentration, depending on the type of species entered on each line. +description real value that defines the slope of the line defining the linear relationship between viscosity and temperature or between viscosity and concentration, depending on the type of species entered on each line. If the value of AUXSPECIESNAME entered on a line corresponds to TEMPERATURE\_SPECIES\_NAME (in the OPTIONS block), this value will be used when VISCOSITY\_FUNC is equal to LINEAR (the default) in the OPTIONS block. When VISCOSITY\_FUNC is set to NONLINEAR, a value for DVISCDC must be specified though it is not used. + +block packagedata +name cviscref +type double precision +shape +tagged false +in_record true +reader urword +longname reference temperature value or reference concentration value +description real value that defines the reference temperature or reference concentration value used for this species in the viscosity equation of state. If AUXSPECIESNAME entered on a line corresponds to TEMPERATURE\_SPECIES\_NAME (in the OPTIONS block), then CVISCREF refers to a reference temperature, otherwise it refers to a reference concentration. + +block packagedata +name modelname +type string +in_record true +tagged false +shape +reader urword +longname modelname +description name of a GWT or GWE model used to simulate a species that will be used in the viscosity equation of state. This name will have no effect if the simulation does not include a GWT or GWE model that corresponds to this GWF model. + +block packagedata +name auxspeciesname +type string +in_record true +tagged false +shape +reader urword +longname auxspeciesname +description name of an auxiliary variable in a GWF stress package that will be used for this species to calculate the viscosity values. If a viscosity value is needed by the Viscosity Package then it will use the temperature or concentration values associated with this AUXSPECIESNAME in the viscosity equation of state. For advanced stress packages (LAK, SFR, MAW, and UZF) that have an associated advanced transport package (LKT, SFT, MWT, and UZT), the FLOW\_PACKAGE\_AUXILIARY\_NAME option in the advanced transport package can be used to transfer simulated temperature or concentration(s) into the flow package auxiliary variable. In this manner, the Viscosity Package can calculate viscosity values for lakes, streams, multi-aquifer wells, and unsaturated zone flow cells using simulated concentrations. + diff --git a/autotest/autotest/temp/dfn/gwf-wel.dfn b/autotest/autotest/temp/dfn/gwf-wel.dfn new file mode 100644 index 00000000..644c3651 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-wel.dfn @@ -0,0 +1,285 @@ +# --------------------- gwf wel options --------------------- +# flopy multi-package +# package-type stress-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'well flow rate'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'well'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'well'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'well'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'well'} +mf6internal ipakcb + +block options +name auto_flow_reduce +type double precision +reader urword +optional true +longname cell fractional thickness for reduced pumping +description keyword and real value that defines the fraction of the cell thickness used as an interval for smoothly adjusting negative pumping rates to 0 in cells with head values less than or equal to the bottom of the cell. Negative pumping rates are adjusted to 0 or a smaller negative value when the head in the cell is equal to or less than the calculated interval above the cell bottom. AUTO\_FLOW\_REDUCE is set to 0.1 if the specified value is less than or equal to zero. By default, negative pumping rates are not reduced during a simulation. This AUTO\_FLOW\_REDUCE option only applies to wells in model cells that are marked as ``convertible'' (ICELLTYPE /= 0) in the Node Property Flow (NPF) input file. Reduction in flow will not occur for wells in cells marked as confined (ICELLTYPE = 0). +mf6internal flowred + +block options +name afrcsv_filerecord +type record auto_flow_reduce_csv fileout afrcsvfile +shape +reader urword +tagged true +optional true +longname +description +mf6internal afrcsv_rec + +block options +name auto_flow_reduce_csv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the AUTO\_FLOW\_REDUCE output option in which a new record is written for each well and for each time step in which the user-requested extraction rate is reduced by the program. +mf6internal afrcsv + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name afrcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write information about well extraction rates that have been reduced by the program. Entries are only written if the extraction rates are reduced. + +block options +name flow_reduction_length +type keyword +reader urword +optional true +longname flow reduction length keyword +description keyword that indicates the AUTO\_FLOW\_REDUCE value is a length instead of a fraction of the cell thickness. A warning will be issued if the FLOW\_REDUCTION\_LENGTH option is specified but the AUTO\_FLOW\_REDUCE option is not specified in the options block. The program will terminate with an error if the FLOW\_REDUCTION\_LENGTH option is specified and the AUTO\_FLOW\_REDUCE value specified in the options block is less than or equal to zero. +mf6internal iflowredlen + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'WEL', '{#2}': '\\ref{table:gwf-obstypetable}'} + +block options +name mover +type keyword +tagged true +reader urword +optional true +longname +description REPLACE mover {'{#1}': 'Well'} + +# --------------------- gwf wel dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of wells +description REPLACE maxbound {'{#1}': 'wells'} + + +# --------------------- gwf wel period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid q aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name q +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname well rate +description is the volumetric well rate. A positive value indicates recharge (injection) and a negative value indicates discharge (extraction). 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. + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'well'} +mf6internal auxvar + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname well name +description REPLACE boundname {'{#1}': 'well'} diff --git a/autotest/autotest/temp/dfn/gwf-welg.dfn b/autotest/autotest/temp/dfn/gwf-welg.dfn new file mode 100644 index 00000000..f6a765a8 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwf-welg.dfn @@ -0,0 +1,233 @@ +# --------------------- gwf wel options --------------------- +# flopy multi-package +# package-type stress-package + +block options +name readarraygrid +type keyword +reader urword +optional false +developmode true +longname use array-based grid input +description indicates that array-based grid input will be used for the well boundary package. This keyword must be specified to use array-based grid input. When READARRAYGRID is specified, values must be provided for every cell within a model grid, even those cells that have an IDOMAIN value less than one. Values assigned to cells with IDOMAIN values less than one are not used and have no effect on simulation results. No data cells should contain the value DNODATA (3.0E+30). +default_value true + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Flow'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'well flow rate'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'well'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'well'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'well'} +mf6internal ipakcb + +block options +name auto_flow_reduce +type double precision +reader urword +optional true +longname cell fractional thickness for reduced pumping +description keyword and real value that defines the fraction of the cell thickness used as an interval for smoothly adjusting negative pumping rates to 0 in cells with head values less than or equal to the bottom of the cell. Negative pumping rates are adjusted to 0 or a smaller negative value when the head in the cell is equal to or less than the calculated interval above the cell bottom. AUTO\_FLOW\_REDUCE is set to 0.1 if the specified value is less than or equal to zero. By default, negative pumping rates are not reduced during a simulation. This AUTO\_FLOW\_REDUCE option only applies to wells in model cells that are marked as ``convertible'' (ICELLTYPE /= 0) in the Node Property Flow (NPF) input file. Reduction in flow will not occur for wells in cells marked as confined (ICELLTYPE = 0). +mf6internal flowred + +block options +name afrcsv_filerecord +type record auto_flow_reduce_csv fileout afrcsvfile +shape +reader urword +tagged true +optional true +longname +description +mf6internal afrcsv_rec + +block options +name auto_flow_reduce_csv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the AUTO\_FLOW\_REDUCE output option in which a new record is written for each well and for each time step in which the user-requested extraction rate is reduced by the program. +mf6internal afrcsv + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name afrcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write information about well extraction rates that have been reduced by the program. Entries are only written if the extraction rates are reduced. + +block options +name flow_reduction_length +type keyword +reader urword +optional true +longname flow reduction length keyword +description keyword that indicates the AUTO\_FLOW\_REDUCE value is a length instead of a fraction of the cell thickness. A warning will be issued if the FLOW\_REDUCTION\_LENGTH option is specified but the AUTO\_FLOW\_REDUCE option is not specified in the options block. The program will terminate with an error if the FLOW\_REDUCTION\_LENGTH option is specified and the AUTO\_FLOW\_REDUCE value specified in the options block is less than or equal to zero. +mf6internal iflowredlen + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'WEL', '{#2}': '\\ref{table:gwf-obstypetable}'} + +block options +name mover +type keyword +tagged true +reader urword +optional true +longname +description REPLACE mover {'{#1}': 'Well'} + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwf wel dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional true +longname maximum number of wells in any stress period +description REPLACE maxbound {'{#1}': 'wells'} + + +# --------------------- gwf wel period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name q +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname well rate +description is the volumetric well rate. A positive value indicates recharge (injection) and a negative value indicates discharge (extraction). +default_value 3.e30 + +block period +name aux +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname well auxiliary variable iaux +description is an array of values for auxiliary variable aux(iaux), where iaux is a value from 1 to naux, and aux(iaux) must be listed as part of the auxiliary variables. A separate array can be specified for each auxiliary variable. If the value specified here for the auxiliary variable is the same as auxmultname, then the well rate array will be multiplied by this array. +mf6internal auxvar diff --git a/autotest/autotest/temp/dfn/gwt-adv.dfn b/autotest/autotest/temp/dfn/gwt-adv.dfn new file mode 100644 index 00000000..519f0bc6 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-adv.dfn @@ -0,0 +1,18 @@ +# --------------------- gwt adv options --------------------- + +block options +name scheme +type string +valid central upstream tvd utvd +reader urword +optional true +longname advective scheme +description scheme used to solve the advection term. Can be upstream, central, TVD or UTVD. If not specified, upstream weighting is the default weighting scheme. + +block options +name ats_percel +type double precision +reader urword +optional true +longname fractional cell distance used for time step calculation +description fractional cell distance submitted by the ADV Package to the adaptive time stepping (ATS) package. If ATS\_PERCEL is specified and the ATS Package is active, a time step calculation will be made for each cell based on flow through the cell and cell properties. The largest time step will be calculated such that the advective fractional cell distance (ATS\_PERCEL) is not exceeded for any active cell in the grid. This time-step constraint will be submitted to the ATS Package, perhaps with constraints submitted by other packages, in the calculation of the time step. ATS\_PERCEL must be greater than zero. If a value of zero is specified for ATS\_PERCEL the program will automatically reset it to an internal no data value to indicate that time steps should not be subject to this constraint. \ No newline at end of file diff --git a/autotest/autotest/temp/dfn/gwt-api.dfn b/autotest/autotest/temp/dfn/gwt-api.dfn new file mode 100644 index 00000000..45b770de --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-api.dfn @@ -0,0 +1,100 @@ +# --------------------- gwt api options --------------------- +# flopy multi-package + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'api boundary'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'api boundary'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'api boundary'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save api flows to budget file +description REPLACE save_flows {'{#1}': 'api boundary'} +mf6internal ipakcb + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'api boundary', '{#2}': '\\ref{table:gwt-obstypetable}'} + +block options +name mover +type keyword +tagged true +reader urword +optional true +longname +description REPLACE mover {'{#1}': 'api boundary'} + +# --------------------- gwt api dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of user-defined api boundaries +description REPLACE maxbound {'{#1}': 'api boundary'} diff --git a/autotest/autotest/temp/dfn/gwt-cnc.dfn b/autotest/autotest/temp/dfn/gwt-cnc.dfn new file mode 100644 index 00000000..debee8e9 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-cnc.dfn @@ -0,0 +1,213 @@ +# --------------------- gwt cnc options --------------------- +# flopy multi-package + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Transport'} + +block options +name auxmultname +type string +shape +reader urword +optional true +longname name of auxiliary variable for multiplier +description REPLACE auxmultname {'{#1}': 'concentration value'} + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'constant concentration'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'constant concentration'} +mf6internal iprpak + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'constant concentration'} +mf6internal iprflow + +block options +name save_flows +type keyword +reader urword +optional true +longname save constant concentration flows to budget file +description REPLACE save_flows {'{#1}': 'constant concentration'} +mf6internal ipakcb + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname time series keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'CNC', '{#2}': '\\ref{table:gwt-obstypetable}'} + + +# --------------------- gwt cnc dimensions --------------------- + +block dimensions +name maxbound +type integer +reader urword +optional false +longname maximum number of constant concentrations +description REPLACE maxbound {'{#1}': 'constant concentrations'} + + +# --------------------- gwt cnc period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name stress_period_data +type recarray cellid conc aux boundname +shape (maxbound) +reader urword +longname +description +mf6internal spd + +block period +name cellid +type integer +shape (ncelldim) +tagged false +in_record true +reader urword +longname cell identifier +description REPLACE cellid {} + +block period +name conc +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname constant concentration value +description is the constant concentration value. 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. +mf6internal tspvar + +block period +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +optional true +time_series true +longname auxiliary variables +description REPLACE aux {'{#1}': 'constant concentration'} +mf6internal auxvar + +block period +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname constant concentration name +description REPLACE boundname {'{#1}': 'constant concentration'} diff --git a/autotest/autotest/temp/dfn/gwt-dis.dfn b/autotest/autotest/temp/dfn/gwt-dis.dfn new file mode 100644 index 00000000..7be72617 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-dis.dfn @@ -0,0 +1,239 @@ +# --------------------- gwt dis options --------------------- +# mf6 subpackage utl-ncf + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position of the model grid origin +description x-position of the lower-left corner of the model grid. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position of the model grid origin +description y-position of the lower-left corner of the model grid. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the lower-left corner of the model grid. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name ncf_filerecord +type record ncf6 filein ncf6_filename +reader urword +tagged true +optional true +longname +description + +block options +name ncf6 +type keyword +in_record true +reader urword +tagged true +optional false +longname ncf keyword +description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ncf6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of NCF information +description defines a NetCDF configuration (NCF) input file. +extended true + +# --------------------- gwt dis dimensions --------------------- + +block dimensions +name nlay +type integer +reader urword +optional false +longname number of layers +description is the number of layers in the model grid. +default_value 1 + +block dimensions +name nrow +type integer +reader urword +optional false +longname number of rows +description is the number of rows in the model grid. +default_value 2 + +block dimensions +name ncol +type integer +reader urword +optional false +longname number of columns +description is the number of columns in the model grid. +default_value 2 + +# --------------------- gwt dis griddata --------------------- + +block griddata +name delr +type double precision +shape (ncol) +reader readarray +netcdf true +longname spacing along a row +description is the column spacing in the row direction. +default_value 1.0 + +block griddata +name delc +type double precision +shape (nrow) +reader readarray +netcdf true +longname spacing along a column +description is the row spacing in the column direction. +default_value 1.0 + +block griddata +name top +type double precision +shape (ncol, nrow) +reader readarray +netcdf true +longname cell top elevation +description is the top elevation for each cell in the top model layer. +default_value 1.0 + +block griddata +name botm +type double precision +shape (ncol, nrow, nlay) +reader readarray +netcdf true +layered true +longname cell bottom elevation +description is the bottom elevation for each cell. +default_value 0. + +block griddata +name idomain +type integer +shape (ncol, nrow, nlay) +reader readarray +layered true +netcdf true +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. + + diff --git a/autotest/autotest/temp/dfn/gwt-disu.dfn b/autotest/autotest/temp/dfn/gwt-disu.dfn new file mode 100644 index 00000000..eacf163b --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-disu.dfn @@ -0,0 +1,337 @@ +# --------------------- gwt disu options --------------------- + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position origin of the model grid coordinate system +description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position origin of the model grid coordinate system +description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name vertical_offset_tolerance +type double precision +reader urword +optional true +default_value 0.0 +longname vertical length dimension for top and bottom checking +description checks are performed to ensure that the top of a cell is not higher than the bottom of an overlying cell. This option can be used to specify the tolerance that is used for checking. If top of a cell is above the bottom of an overlying cell by a value less than this tolerance, then the program will not terminate with an error. The default value is zero. This option should generally not be used. +mf6internal voffsettol + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +# --------------------- gwt disu dimensions --------------------- + +block dimensions +name nodes +type integer +reader urword +optional false +longname number of layers +description is the number of cells in the model grid. + +block dimensions +name nja +type integer +reader urword +optional false +longname number of columns +description is the sum of the number of connections and NODES. When calculating the total number of connections, the connection between cell n and cell m is considered to be different from the connection between cell m and cell n. Thus, NJA is equal to the total number of connections, including n to m and m to n, and the total number of cells. + +block dimensions +name nvert +type integer +reader urword +optional true +longname number of vertices +description is the total number of (x, y) vertex pairs used to define the plan-view shape of each cell in the model grid. If NVERT is not specified or is specified as zero, then the VERTICES and CELL2D blocks below are not read. NVERT and the accompanying VERTICES and CELL2D blocks should be specified for most simulations. If the XT3D or SAVE\_SPECIFIC\_DISCHARGE options are specified in the NPF Package, then this information is required. + +# --------------------- gwt disu griddata --------------------- + +block griddata +name top +type double precision +shape (nodes) +reader readarray +longname cell top elevation +description is the top elevation for each cell in the model grid. + +block griddata +name bot +type double precision +shape (nodes) +reader readarray +longname cell bottom elevation +description is the bottom elevation for each cell. + +block griddata +name area +type double precision +shape (nodes) +reader readarray +longname cell surface area +description is the cell surface area (in plan view). + +block griddata +name idomain +type integer +shape (nodes) +reader readarray +layered false +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1 or greater, the cell exists in the simulation. IDOMAIN values of -1 cannot be specified for the DISU Package. + +# --------------------- gwt disu connectiondata --------------------- + +block connectiondata +name iac +type integer +shape (nodes) +reader readarray +longname number of cell connections +description is the number of connections (plus 1) for each cell. The sum of all the entries in IAC must be equal to NJA. + +block connectiondata +name ja +type integer +shape (nja) +reader readarray +longname grid connectivity +description is a list of cell number (n) followed by its connecting cell numbers (m) for each of the m cells connected to cell n. The number of values to provide for cell n is IAC(n). This list is sequentially provided for the first to the last cell. The first value in the list must be cell n itself, and the remaining cells must be listed in an increasing order (sorted from lowest number to highest). Note that the cell and its connections are only supplied for the GWT cells and their connections to the other GWT cells. Also note that the JA list input may be divided such that every node and its connectivity list can be on a separate line for ease in readability of the file. To further ease readability of the file, the node number of the cell whose connectivity is subsequently listed, may be expressed as a negative number, the sign of which is subsequently converted to positive by the code. +numeric_index true +jagged_array iac + +block connectiondata +name ihc +type integer +shape (nja) +reader readarray +longname connection type +description is an index array indicating the direction between node n and all of its m connections. If IHC = 0 then cell n and cell m are connected in the vertical direction. Cell n overlies cell m if the cell number for n is less than m; cell m overlies cell n if the cell number for m is less than n. If IHC = 1 then cell n and cell m are connected in the horizontal direction. If IHC = 2 then cell n and cell m are connected in the horizontal direction, and the connection is vertically staggered. A vertically staggered connection is one in which a cell is horizontally connected to more than one cell in a horizontal connection. +jagged_array iac + +block connectiondata +name cl12 +type double precision +shape (nja) +reader readarray +longname connection lengths +description is the array containing connection lengths between the center of cell n and the shared face with each adjacent m cell. +jagged_array iac + +block connectiondata +name hwva +type double precision +shape (nja) +reader readarray +longname connection lengths +description is a symmetric array of size NJA. For horizontal connections, entries in HWVA are the horizontal width perpendicular to flow. For vertical connections, entries in HWVA are the vertical area for flow. Thus, values in the HWVA array contain dimensions of both length and area. Entries in the HWVA array have a one-to-one correspondence with the connections specified in the JA array. Likewise, there is a one-to-one correspondence between entries in the HWVA array and entries in the IHC array, which specifies the connection type (horizontal or vertical). Entries in the HWVA array must be symmetric; the program will terminate with an error if the value for HWVA for an n to m connection does not equal the value for HWVA for the corresponding n to m connection. +jagged_array iac + +block connectiondata +name angldegx +type double precision +optional true +shape (nja) +reader readarray +longname angle of face normal to connection +description is the angle (in degrees) between the horizontal x-axis and the outward normal to the face between a cell and its connecting cells. The angle varies between zero and 360.0 degrees, where zero degrees points in the positive x-axis direction, and 90 degrees points in the positive y-axis direction. ANGLDEGX is only needed if horizontal anisotropy is specified in the NPF Package, if the XT3D option is used in the NPF Package, or if the SAVE\_SPECIFIC\_DISCHARGE option is specified in the NPF Package. ANGLDEGX does not need to be specified if these conditions are not met. ANGLDEGX is of size NJA; values specified for vertical connections and for the diagonal position are not used. Note that ANGLDEGX is read in degrees, which is different from MODFLOW-USG, which reads a similar variable (ANGLEX) in radians. +jagged_array iac + +# --------------------- gwt disu vertices --------------------- + +block vertices +name vertices +type recarray iv xv yv +shape (nvert) +reader urword +optional true +longname vertices data +description + +block vertices +name iv +type integer +in_record true +tagged false +reader urword +optional false +longname vertex number +description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. +numeric_index true + +block vertices +name xv +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for vertex +description is the x-coordinate for the vertex. + +block vertices +name yv +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for vertex +description is the y-coordinate for the vertex. + + +# --------------------- gwt disu cell2d --------------------- + +block cell2d +name cell2d +type recarray icell2d xc yc ncvert icvert +shape (nodes) +reader urword +optional true +longname cell2d data +description + +block cell2d +name icell2d +type integer +in_record true +tagged false +reader urword +optional false +longname cell2d number +description is the cell2d number. Records in the CELL2D block must be listed in consecutive order from 1 to NODES. +numeric_index true + +block cell2d +name xc +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for cell center +description is the x-coordinate for the cell center. + +block cell2d +name yc +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for cell center +description is the y-coordinate for the cell center. + +block cell2d +name ncvert +type integer +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. + +block cell2d +name icvert +type integer +shape (ncvert) +in_record true +tagged false +reader urword +optional false +longname array of vertex numbers +description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. +numeric_index true diff --git a/autotest/autotest/temp/dfn/gwt-disv.dfn b/autotest/autotest/temp/dfn/gwt-disv.dfn new file mode 100644 index 00000000..29ad0e5a --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-disv.dfn @@ -0,0 +1,320 @@ +# --------------------- gwt disv options --------------------- +# mf6 subpackage utl-ncf + +block options +name length_units +type string +reader urword +optional true +longname model length units +description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. + +block options +name nogrb +type keyword +reader urword +optional true +longname do not write binary grid file +description keyword to deactivate writing of the binary grid file. + +block options +name grb_filerecord +type record grb6 fileout grb6_filename +reader urword +tagged true +optional true +longname +description + +block options +name grb6 +type keyword +in_record true +reader urword +tagged true +optional false +longname grb keyword +description keyword to specify that record corresponds to a binary grid file. + +block options +name fileout +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name grb6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of GRB information +description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. + +block options +name xorigin +type double precision +reader urword +optional true +longname x-position origin of the model grid coordinate system +description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name yorigin +type double precision +reader urword +optional true +longname y-position origin of the model grid coordinate system +description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name angrot +type double precision +reader urword +optional true +longname rotation angle +description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +block options +name crs +type string +shape lenbigline +preserve_case true +reader urword +optional true +developmode true +longname CRS user input string +description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. + +block options +name ncf_filerecord +type record ncf6 filein ncf6_filename +reader urword +tagged true +optional true +longname +description + +block options +name ncf6 +type keyword +in_record true +reader urword +tagged true +optional false +longname ncf keyword +description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ncf6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of NCF information +description defines a NetCDF configuration (NCF) input file. +extended true + +# --------------------- gwt disv dimensions --------------------- + +block dimensions +name nlay +type integer +reader urword +optional false +longname number of layers +description is the number of layers in the model grid. + +block dimensions +name ncpl +type integer +reader urword +optional false +longname number of cells per layer +description is the number of cells per layer. This is a constant value for the grid and it applies to all layers. + +block dimensions +name nvert +type integer +reader urword +optional false +longname number of columns +description is the total number of (x, y) vertex pairs used to characterize the horizontal configuration of the model grid. + +# --------------------- gwt disv griddata --------------------- + +block griddata +name top +type double precision +shape (ncpl) +reader readarray +netcdf true +longname model top elevation +description is the top elevation for each cell in the top model layer. + +block griddata +name botm +type double precision +shape (ncpl, nlay) +reader readarray +layered true +netcdf true +longname model bottom elevation +description is the bottom elevation for each cell. + +block griddata +name idomain +type integer +shape (ncpl, nlay) +reader readarray +layered true +netcdf true +optional true +longname idomain existence array +description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. + + +# --------------------- gwt disv vertices --------------------- + +block vertices +name vertices +type recarray iv xv yv +shape (nvert) +reader urword +optional false +longname vertices data +description + +block vertices +name iv +type integer +in_record true +tagged false +reader urword +optional false +longname vertex number +description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. +numeric_index true + +block vertices +name xv +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for vertex +description is the x-coordinate for the vertex. + +block vertices +name yv +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for vertex +description is the y-coordinate for the vertex. + + +# --------------------- gwt disv cell2d --------------------- + +block cell2d +name cell2d +type recarray icell2d xc yc ncvert icvert +shape (ncpl) +reader urword +optional false +longname cell2d data +description + +block cell2d +name icell2d +type integer +in_record true +tagged false +reader urword +optional false +longname cell2d number +description is the CELL2D number. Records in the CELL2D block must be listed in consecutive order from the first to the last. +numeric_index true + +block cell2d +name xc +type double precision +in_record true +tagged false +reader urword +optional false +longname x-coordinate for cell center +description is the x-coordinate for the cell center. + +block cell2d +name yc +type double precision +in_record true +tagged false +reader urword +optional false +longname y-coordinate for cell center +description is the y-coordinate for the cell center. + +block cell2d +name ncvert +type integer +in_record true +tagged false +reader urword +optional false +longname number of cell vertices +description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. + +block cell2d +name icvert +type integer +shape (ncvert) +in_record true +tagged false +reader urword +optional false +longname array of vertex numbers +description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. Cells that are connected must share vertices. +numeric_index true diff --git a/autotest/autotest/temp/dfn/gwt-dsp.dfn b/autotest/autotest/temp/dfn/gwt-dsp.dfn new file mode 100644 index 00000000..d8b5f590 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-dsp.dfn @@ -0,0 +1,106 @@ +# --------------------- gwt dsp options --------------------- + +block options +name xt3d_off +type keyword +shape +reader urword +optional true +longname deactivate xt3d +description deactivate the xt3d method and use the faster and less accurate approximation. This option may provide a fast and accurate solution under some circumstances, such as when flow aligns with the model grid, there is no mechanical dispersion, or when the longitudinal and transverse dispersivities are equal. This option may also be used to assess the computational demand of the XT3D approach by noting the run time differences with and without this option on. + +block options +name xt3d_rhs +type keyword +shape +reader urword +optional true +longname xt3d on right-hand side +description add xt3d terms to right-hand side, when possible. This option uses less memory, but may require more iterations. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwt dsp griddata --------------------- + +block griddata +name diffc +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname effective molecular diffusion coefficient +description effective molecular diffusion coefficient. + +block griddata +name alh +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname longitudinal dispersivity in horizontal direction +description longitudinal dispersivity in horizontal direction. If flow is strictly horizontal, then this is the longitudinal dispersivity that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. + +block griddata +name alv +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname longitudinal dispersivity in vertical direction +description longitudinal dispersivity in vertical direction. If flow is strictly vertical, then this is the longitudinal dispsersivity value that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ALH. + +block griddata +name ath1 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity in horizontal direction +description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the second ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the y direction. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. + +block griddata +name ath2 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity in horizontal direction +description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the third ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the z direction. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH1. + +block griddata +name atv +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname transverse dispersivity when flow is in vertical direction +description transverse dispersivity when flow is in vertical direction. If flow is strictly vertical and directed in the z direction, then this value controls spreading in the x and y directions. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH2. diff --git a/autotest/autotest/temp/dfn/gwt-fmi.dfn b/autotest/autotest/temp/dfn/gwt-fmi.dfn new file mode 100644 index 00000000..f7402caf --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-fmi.dfn @@ -0,0 +1,59 @@ +# --------------------- gwt fmi options --------------------- + +block options +name save_flows +type keyword +reader urword +optional true +longname save calculated flow imbalance correction to budget file +description REPLACE save_flows {'{#1}': 'FMI'} + +block options +name flow_imbalance_correction +type keyword +reader urword +optional true +mf6internal imbalancecorrect +longname correct for flow imbalance +description correct for an imbalance in flows by assuming that any residual flow error comes in or leaves at the concentration of the cell. When this option is activated, the GWT Model budget written to the listing file will contain two additional entries: FLOW-ERROR and FLOW-CORRECTION. These two entries will be equal but opposite in sign. The FLOW-CORRECTION term is a mass flow that is added to offset the error caused by an imprecise flow balance. If these terms are not relatively small, the flow model should be rerun with stricter convergence tolerances. + +# --------------------- gwt fmi packagedata --------------------- + +block packagedata +name packagedata +type recarray flowtype filein fname +reader urword +optional true +longname flowtype list +description + +block packagedata +name flowtype +in_record true +type string +tagged false +reader urword +longname flow type +description is the word GWFBUDGET, GWFHEAD, GWFMOVER or the name of an advanced GWF stress package. If GWFBUDGET is specified, then the corresponding file must be a budget file. If GWFHEAD is specified, the file must be a head file. If GWFGRID is specified, the file must be a binary grid file. If an advanced GWF stress package name appears then the corresponding file must be the budget file saved by a LAK, SFR, MAW or UZF Package. + +block packagedata +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block packagedata +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing flows. The path to the file should be included if the file is not located in the folder where the program was run. + diff --git a/autotest/autotest/temp/dfn/gwt-ic.dfn b/autotest/autotest/temp/dfn/gwt-ic.dfn new file mode 100644 index 00000000..f96cb7da --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-ic.dfn @@ -0,0 +1,33 @@ +# --------------------- gwt ic options --------------------- + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwt ic griddata --------------------- + +block griddata +name strt +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname starting concentration +description is the initial (starting) concentration---that is, concentration at the beginning of the GWT Model simulation. STRT must be specified for all GWT Model simulations. One value is read for every model cell. +default_value 0.0 diff --git a/autotest/autotest/temp/dfn/gwt-ist.dfn b/autotest/autotest/temp/dfn/gwt-ist.dfn new file mode 100644 index 00000000..287421ce --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-ist.dfn @@ -0,0 +1,377 @@ +# --------------------- gwt ist options --------------------- +# flopy multi-package + +block options +name save_flows +type keyword +reader urword +optional true +longname save calculated flows to budget file +description REPLACE save_flows {'{#1}': 'IST'} + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +mf6internal budfilerec +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +mf6internal budcsvfilerec +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name sorption +type string +valid linear freundlich langmuir +reader urword +optional true +longname activate sorption +description is a text keyword to indicate that sorption will be activated. Valid sorption options include LINEAR, FREUNDLICH, and LANGMUIR. Use of this keyword requires that BULK\_DENSITY and DISTCOEF are specified in the GRIDDATA block. If sorption is specified as FREUNDLICH or LANGMUIR then SP2 is also required in the GRIDDATA block. The sorption option must be consistent with the sorption option specified in the MST Package or the program will terminate with an error. + +block options +name first_order_decay +type keyword +reader urword +optional true +longname activate first-order decay +mf6internal order1_decay +description is a text keyword to indicate that first-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. + +block options +name zero_order_decay +type keyword +reader urword +optional true +mf6internal order0_decay +longname activate zero-order decay +description is a text keyword to indicate that zero-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. + +block options +name cim_filerecord +type record cim fileout cimfile +shape +reader urword +tagged true +optional true +mf6internal cimfilerec +longname +description + +block options +name cim +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname cim keyword +mf6internal cimopt +description keyword to specify that record corresponds to immobile concentration. + +block options +name cimfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write immobile concentrations. This file is a binary file that has the same format and structure as a binary head and concentration file. The value for the text variable written to the file is CIM. Immobile domain concentrations will be written to this file at the same interval as mobile domain concentrations are saved, as specified in the GWT Model Output Control file. + +block options +name cimprintrecord +type record cim print_format formatrecord +shape +reader urword +optional true +longname +description + +block options +name print_format +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to indicate that a print format follows +description keyword to specify format for printing to the listing file. + +block options +name formatrecord +type record columns width digits format +shape +in_record true +reader urword +tagged +optional false +longname +description + +block options +name columns +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of columns +description number of columns for writing data. + +block options +name width +type integer +shape +in_record true +reader urword +tagged true +optional +longname width for each number +description width for writing each number. + +block options +name digits +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of digits +description number of digits to use for writing a number. + +block options +name format +type string +shape +in_record true +reader urword +tagged false +optional false +longname write format +description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. + +block options +name sorbate_filerecord +type record sorbate fileout sorbatefile +shape +reader urword +tagged true +optional true +mf6internal sorbatefilerec +longname +description + +block options +name sorbate +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname sorbate keyword +description keyword to specify that record corresponds to immobile sorbate concentration. + +block options +name sorbatefile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write immobile sorbate concentration information. Immobile sorbate concentrations will be written whenever aqueous immobile concentrations are saved, as determined by settings in the Output Control option. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwt ist griddata --------------------- + +block griddata +name porosity +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname porosity of the immobile domain +description porosity of the immobile domain specified as the immobile domain pore volume per immobile domain volume. + +block griddata +name volfrac +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname volume fraction of this immobile domain +description fraction of the cell volume that consists of this immobile domain. The sum of all immobile domain volume fractions must be less than one. + +block griddata +name zetaim +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname mass transfer rate coefficient between the mobile and immobile domains +description mass transfer rate coefficient between the mobile and immobile domains, in dimensions of per time. + +block griddata +name cim +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname initial concentration of the immobile domain +description initial concentration of the immobile domain in mass per length cubed. If CIM is not specified, then it is assumed to be zero. + +block griddata +name decay +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname first rate coefficient +description is the rate coefficient for first or zero-order decay for the aqueous phase of the immobile domain. A negative value indicates solute production. The dimensions of decay for first-order decay is one over time. The dimensions of decay for zero-order decay is mass per length cubed per time. Decay will have no effect on simulation results unless either first- or zero-order decay is specified in the options block. + +block griddata +name decay_sorbed +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname second rate coefficient +description is the rate coefficient for first or zero-order decay for the sorbed phase of the immobile domain. A negative value indicates solute production. The dimensions of decay\_sorbed for first-order decay is one over time. The dimensions of decay\_sorbed for zero-order decay is mass of solute per mass of aquifer per time. If decay\_sorbed is not specified and both decay and sorption are active, then the program will terminate with an error. decay\_sorbed will have no effect on simulation results unless the SORPTION keyword and either first- or zero-order decay are specified in the options block. + +block griddata +name bulk_density +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname bulk density +description is the bulk density of this immobile domain in mass per length cubed. Bulk density is defined as the immobile domain solid mass per volume of the immobile domain. bulk\_density is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, bulk\_density will have no effect on simulation results. + +block griddata +name distcoef +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname distribution coefficient +description is the distribution coefficient for the equilibrium-controlled linear sorption isotherm in dimensions of length cubed per mass. distcoef is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, distcoef will have no effect on simulation results. + +block griddata +name sp2 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname second sorption parameter +description is the exponent for the Freundlich isotherm and the sorption capacity for the Langmuir isotherm. sp2 is not required unless the SORPTION keyword is specified in the options block and sorption is specified as FREUNDLICH or LANGMUIR. If the SORPTION keyword is not specified in the options block, or if sorption is specified as LINEAR, sp2 will have no effect on simulation results. diff --git a/autotest/autotest/temp/dfn/gwt-lkt.dfn b/autotest/autotest/temp/dfn/gwt-lkt.dfn new file mode 100644 index 00000000..f32d282d --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-lkt.dfn @@ -0,0 +1,460 @@ +# --------------------- gwt lkt options --------------------- +# flopy multi-package + +block options +name flow_package_name +type string +shape +reader urword +optional true +longname keyword to specify name of corresponding flow package +description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWT name file). + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Transport'} + +block options +name flow_package_auxiliary_name +type string +shape +reader urword +optional true +longname keyword to specify name of concentration auxiliary variable in flow package +description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated concentrations from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'lake'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'lake'} + +block options +name print_concentration +type keyword +reader urword +optional true +longname print calculated stages to listing file +description REPLACE print_concentration {'{#1}': 'lake', '{#2}': 'concentration', '{#3}': 'CONCENTRATION'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'lake'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save lake flows to budget file +description REPLACE save_flows {'{#1}': 'lake'} + +block options +name concentration_filerecord +type record concentration fileout concfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name concentration +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname stage keyword +description keyword to specify that record corresponds to concentration. + +block options +name concfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write concentration information. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'LKT', '{#2}': '\\ref{table:gwt-obstypetable}'} + + +# --------------------- gwt lkt packagedata --------------------- + +block packagedata +name packagedata +type recarray ifno strt aux boundname +shape (maxbound) +reader urword +longname +description + +block packagedata +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname lake number for this entry +description integer value that defines the feature (lake) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NLAKES. Lake information must be specified for every lake or the program will terminate with an error. The program will also terminate with an error if information for a lake is specified more than once. +numeric_index true + +block packagedata +name strt +type double precision +shape +tagged false +in_record true +reader urword +longname starting lake concentration +description real value that defines the starting concentration for the lake. + +block packagedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +time_series true +optional true +longname auxiliary variables +description REPLACE aux {'{#1}': 'lake'} + +block packagedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname lake name +description REPLACE boundname {'{#1}': 'lake'} + + +# --------------------- gwt lkt period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name lakeperioddata +type recarray ifno laksetting +shape +reader urword +longname +description + +block period +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname lake number for this entry +description integer value that defines the feature (lake) number associated with the specified PERIOD data on the line. IFNO must be greater than zero and less than or equal to NLAKES. +numeric_index true + +block period +name laksetting +type keystring status concentration rainfall evaporation runoff ext-inflow auxiliaryrecord +shape +tagged false +in_record true +reader urword +longname +description line of information that is parsed into a keyword and values. Keyword values that can be used to start the LAKSETTING string include: STATUS, CONCENTRATION, RAINFALL, EVAPORATION, RUNOFF, EXT-INFLOW, and AUXILIARY. These settings are used to assign the concentration of associated with the corresponding flow terms. Concentrations cannot be specified for all flow terms. For example, the Lake Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the lake at the calculated concentration of the lake. + +block period +name status +type string +shape +tagged true +in_record true +reader urword +longname lake concentration status +description keyword option to define lake status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that concentration will be calculated for the lake. If a lake is inactive, then there will be no solute mass fluxes into or out of the lake and the inactive value will be written for the lake concentration. If a lake is constant, then the concentration for the lake will be fixed at the user specified value. + +block period +name concentration +type string +shape +tagged true +in_record true +time_series true +reader urword +longname lake concentration +description real or character value that defines the concentration for the lake. The specified CONCENTRATION is only applied if the lake is a constant concentration lake. 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. + +block period +name rainfall +type string +shape +tagged true +in_record true +reader urword +time_series true +longname rainfall concentration +description real or character value that defines the rainfall solute concentration $(ML^{-3})$ for the lake. 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. + +block period +name evaporation +type string +shape +tagged true +in_record true +reader urword +time_series true +longname evaporation concentration +description real or character value that defines the concentration of evaporated water $(ML^{-3})$ for the lake. If this concentration value is larger than the simulated concentration in the lake, then the evaporated water will be removed at the same concentration as the lake. 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. + +block period +name runoff +type string +shape +tagged true +in_record true +reader urword +time_series true +longname runoff concentration +description real or character value that defines the concentration of runoff $(ML^{-3})$ for the lake. Value must be greater than or equal to zero. 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. + +block period +name ext-inflow +type string +shape +tagged true +in_record true +reader urword +time_series true +longname ext-inflow concentration +description real or character value that defines the concentration of external inflow $(ML^{-3})$ for the lake. Value must be greater than or equal to zero. 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. + +block period +name auxiliaryrecord +type record auxiliary auxname auxval +shape +tagged +in_record true +reader urword +longname +description + +block period +name auxiliary +type keyword +shape +in_record true +reader urword +longname +description keyword for specifying auxiliary variable. + +block period +name auxname +type string +shape +tagged false +in_record true +reader urword +longname +description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. + +block period +name auxval +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname auxiliary variable value +description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwt-mst.dfn b/autotest/autotest/temp/dfn/gwt-mst.dfn new file mode 100644 index 00000000..8e97da15 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-mst.dfn @@ -0,0 +1,168 @@ +# --------------------- gwt mst options --------------------- + +block options +name save_flows +type keyword +reader urword +optional true +longname save calculated flows to budget file +description REPLACE save_flows {'{#1}': 'MST'} + +block options +name first_order_decay +type keyword +reader urword +optional true +mf6internal order1_decay +longname activate first-order decay +description is a text keyword to indicate that first-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. + +block options +name zero_order_decay +type keyword +reader urword +optional true +mf6internal order0_decay +longname activate zero-order decay +description is a text keyword to indicate that zero-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. + +block options +name sorption +type string +valid linear freundlich langmuir +reader urword +optional true +longname activate sorption +description is a text keyword to indicate that sorption will be activated. Valid sorption options include LINEAR, FREUNDLICH, and LANGMUIR. Use of this keyword requires that BULK\_DENSITY and DISTCOEF are specified in the GRIDDATA block. If sorption is specified as FREUNDLICH or LANGMUIR then SP2 is also required in the GRIDDATA block. + +block options +name sorbate_filerecord +type record sorbate fileout sorbatefile +shape +reader urword +tagged true +optional true +longname +description +mf6internal sorbate_rec + +block options +name sorbate +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname sorbate keyword +description keyword to specify that record corresponds to sorbate concentration. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name sorbatefile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write sorbate concentration information. Sorbate concentrations will be written whenever aqueous concentrations are saved, as determined by settings in the Output Control option. + +block options +name export_array_ascii +type keyword +reader urword +optional true +mf6internal export_ascii +longname export array variables to layered ascii files. +description keyword that specifies input griddata arrays should be written to layered ascii output files. + +block options +name export_array_netcdf +type keyword +reader urword +optional true +mf6internal export_nc +longname export array variables to netcdf output files. +description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. +extended true + +# --------------------- gwt mst griddata --------------------- + +block griddata +name porosity +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +longname porosity +description is the mobile domain porosity, defined as the mobile domain pore volume per mobile domain volume. Additional information on porosity within the context of mobile and immobile domain transport simulations is included in the MODFLOW 6 Supplemental Technical Information document. + +block griddata +name decay +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname aqueous phase decay rate coefficient +description is the rate coefficient for first or zero-order decay for the aqueous phase of the mobile domain. A negative value indicates solute production. The dimensions of decay for first-order decay is one over time. The dimensions of decay for zero-order decay is mass per length cubed per time. decay will have no effect on simulation results unless either first- or zero-order decay is specified in the options block. + +block griddata +name decay_sorbed +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname sorbed phase decay rate coefficient +description is the rate coefficient for first or zero-order decay for the sorbed phase of the mobile domain. A negative value indicates solute production. The dimensions of decay\_sorbed for first-order decay is one over time. The dimensions of decay\_sorbed for zero-order decay is mass of solute per mass of aquifer per time. If decay\_sorbed is not specified and both decay and sorption are active, then the program will terminate with an error. decay\_sorbed will have no effect on simulation results unless the SORPTION keyword and either first- or zero-order decay are specified in the options block. + +block griddata +name bulk_density +type double precision +shape (nodes) +reader readarray +optional true +layered true +netcdf true +longname bulk density +description is the bulk density of the aquifer in mass per length cubed. bulk\_density is not required unless the SORPTION keyword is specified. Bulk density is defined as the mobile domain solid mass per mobile domain volume. Additional information on bulk density is included in the MODFLOW 6 Supplemental Technical Information document. + +block griddata +name distcoef +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname distribution coefficient +description is the distribution coefficient for the equilibrium-controlled linear sorption isotherm in dimensions of length cubed per mass. If the Freunchlich isotherm is specified, then discoef is the Freundlich constant. If the Langmuir isotherm is specified, then distcoef is the Langmuir constant. distcoef is not required unless the SORPTION keyword is specified. + +block griddata +name sp2 +type double precision +shape (nodes) +reader readarray +layered true +netcdf true +optional true +longname second sorption parameter +description is the exponent for the Freundlich isotherm and the sorption capacity for the Langmuir isotherm. sp2 is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, sp2 will have no effect on simulation results. + diff --git a/autotest/autotest/temp/dfn/gwt-mvt.dfn b/autotest/autotest/temp/dfn/gwt-mvt.dfn new file mode 100644 index 00000000..4423589f --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-mvt.dfn @@ -0,0 +1,106 @@ +# --------------------- gwt mvt options --------------------- +# flopy subpackage mvt_filerecord mvt perioddata perioddata +# flopy parent_name_type parent_model_or_package MFModel/MFPackage + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'mover'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'lake'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save lake flows to budget file +description REPLACE save_flows {'{#1}': 'lake'} + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + + diff --git a/autotest/autotest/temp/dfn/gwt-mwt.dfn b/autotest/autotest/temp/dfn/gwt-mwt.dfn new file mode 100644 index 00000000..c3106c4f --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-mwt.dfn @@ -0,0 +1,427 @@ +# --------------------- gwt mwt options --------------------- +# flopy multi-package + +block options +name flow_package_name +type string +shape +reader urword +optional true +longname keyword to specify name of corresponding flow package +description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWT name file). + +block options +name auxiliary +type string +shape (naux) +reader urword +optional true +longname keyword to specify aux variables +description REPLACE auxnames {'{#1}': 'Groundwater Transport'} + +block options +name flow_package_auxiliary_name +type string +shape +reader urword +optional true +longname keyword to specify name of concentration auxiliary variable in flow package +description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated concentrations from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. + +block options +name boundnames +type keyword +shape +reader urword +optional true +longname +description REPLACE boundnames {'{#1}': 'well'} + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'well'} + +block options +name print_concentration +type keyword +reader urword +optional true +longname print calculated concentrations to listing file +description REPLACE print_concentration {'{#1}': 'well', '{#2}': 'concentration', '{#3}': 'CONCENTRATION'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'well'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save well flows to budget file +description REPLACE save_flows {'{#1}': 'well'} + +block options +name concentration_filerecord +type record concentration fileout concfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name concentration +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname stage keyword +description keyword to specify that record corresponds to concentration. + +block options +name concfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write concentration information. + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the binary output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name ts_filerecord +type record ts6 filein ts6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name ts6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname head keyword +description keyword to specify that record corresponds to a time-series file. + +block options +name filein +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name ts6_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname file name of time series information +description REPLACE timeseriesfile {} + +block options +name obs_filerecord +type record obs6 filein obs6_filename +shape +reader urword +tagged true +optional true +longname +description + +block options +name obs6 +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname obs keyword +description keyword to specify that record corresponds to an observations file. + +block options +name obs6_filename +type string +preserve_case true +in_record true +tagged false +reader urword +optional false +longname obs6 input filename +description REPLACE obs6_filename {'{#1}': 'MWT', '{#2}': '\\ref{table:gwt-obstypetable}'} + + +# --------------------- gwt mwt packagedata --------------------- + +block packagedata +name packagedata +type recarray ifno strt aux boundname +shape (maxbound) +reader urword +longname +description + +block packagedata +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname well number for this entry +description integer value that defines the feature (well) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NMAWWELLS. Well information must be specified for every well or the program will terminate with an error. The program will also terminate with an error if information for a well is specified more than once. +numeric_index true + +block packagedata +name strt +type double precision +shape +tagged false +in_record true +reader urword +longname starting well concentration +description real value that defines the starting concentration for the well. + +block packagedata +name aux +type double precision +in_record true +tagged false +shape (naux) +reader urword +time_series true +optional true +longname auxiliary variables +description REPLACE aux {'{#1}': 'well'} + +block packagedata +name boundname +type string +shape +tagged false +in_record true +reader urword +optional true +longname well name +description REPLACE boundname {'{#1}': 'well'} + + +# --------------------- gwt mwt period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name mwtperioddata +type recarray ifno mwtsetting +shape +reader urword +longname +description + +block period +name ifno +type integer +shape +tagged false +in_record true +reader urword +longname well number for this entry +description integer value that defines the feature (well) number associated with the specified PERIOD data on the line. IFNO must be greater than zero and less than or equal to NMAWWELLS. +numeric_index true + +block period +name mwtsetting +type keystring status concentration rate auxiliaryrecord +shape +tagged false +in_record true +reader urword +longname +description line of information that is parsed into a keyword and values. Keyword values that can be used to start the MWTSETTING string include: STATUS, CONCENTRATION, RATE, and AUXILIARY. These settings are used to assign the concentration associated with the corresponding flow terms. Concentrations cannot be specified for all flow terms. For example, the Multi-Aquifer Well Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the well at the calculated concentration of the well. + +block period +name status +type string +shape +tagged true +in_record true +reader urword +longname well concentration status +description keyword option to define well status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that concentration will be calculated for the well. If a well is inactive, then there will be no solute mass fluxes into or out of the well and the inactive value will be written for the well concentration. If a well is constant, then the concentration for the well will be fixed at the user specified value. + +block period +name concentration +type string +shape +tagged true +in_record true +time_series true +reader urword +longname well concentration +description real or character value that defines the concentration for the well. The specified CONCENTRATION is only applied if the well is a constant concentration well. 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. + +block period +name rate +type string +shape +tagged true +in_record true +reader urword +time_series true +longname well injection concentration +description real or character value that defines the injection solute concentration $(ML^{-3})$ for the well. 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. + +block period +name auxiliaryrecord +type record auxiliary auxname auxval +shape +tagged +in_record true +reader urword +longname +description + +block period +name auxiliary +type keyword +shape +in_record true +reader urword +longname +description keyword for specifying auxiliary variable. + +block period +name auxname +type string +shape +tagged false +in_record true +reader urword +longname +description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. + +block period +name auxval +type double precision +shape +tagged false +in_record true +reader urword +time_series true +longname auxiliary variable value +description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwt-nam.dfn b/autotest/autotest/temp/dfn/gwt-nam.dfn new file mode 100644 index 00000000..9645f870 --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-nam.dfn @@ -0,0 +1,210 @@ +# --------------------- gwt nam options --------------------- + +block options +name list +type string +reader urword +optional true +preserve_case true +longname name of listing file +description is name of the listing file to create for this GWT model. If not specified, then the name of the list file will be the basename of the GWT model name file and the '.lst' extension. For example, if the GWT name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. + +block options +name print_input +type keyword +reader urword +optional true +longname print input to listing file +description REPLACE print_input {'{#1}': 'all model stress package'} + +block options +name print_flows +type keyword +reader urword +optional true +longname print calculated flows to listing file +description REPLACE print_flows {'{#1}': 'all model package'} + +block options +name save_flows +type keyword +reader urword +optional true +longname save flows for all packages to budget file +description REPLACE save_flows {'{#1}': 'all model package'} + +block options +name dependent_variable_scaling +type keyword +reader urword +optional true +longname flag to scale X and RHS +description flag to scale X and RHS to avoid very large positive or negative dependent variable values +mf6internal idv_scale + +block options +name nc_mesh2d_filerecord +type record netcdf_mesh2d fileout ncmesh2dfile +shape +reader urword +tagged true +optional true +longname +description NetCDF layered mesh fileout record. +mf6internal ncmesh2drec + +block options +name netcdf_mesh2d +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a layered mesh NetCDF file. +extended true + +block options +name nc_structured_filerecord +type record netcdf_structured fileout ncstructfile +shape +reader urword +tagged true +optional true +longname +description NetCDF structured fileout record. +mf6internal ncstructrec + +block options +name netcdf_structured +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to a structured NetCDF file. +mf6internal netcdf_struct +extended true + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name ncmesh2dfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF ugrid layered mesh output file. +extended true + +block options +name ncstructfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the NetCDF structured output file. +extended true + +block options +name nc_filerecord +type record netcdf filein netcdf_filename +reader urword +tagged true +optional true +longname +description NetCDF filerecord + +block options +name netcdf +type keyword +in_record true +reader urword +tagged true +optional false +longname netcdf keyword +description keyword to specify that record corresponds to a NetCDF input file. +extended true + +block options +name filein +type keyword +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an input filename is expected next. + +block options +name netcdf_filename +type string +preserve_case true +in_record true +reader urword +optional false +tagged false +longname netcdf input filename +description defines a NetCDF input file. +mf6internal netcdf_fname +extended true + +# --------------------- gwt nam packages --------------------- + +block packages +name packages +type recarray ftype fname pname +reader urword +optional false +longname package list +description + +block packages +name ftype +in_record true +type string +tagged false +reader urword +longname package type +description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwt}. Ftype may be entered in any combination of uppercase and lowercase. + +block packages +name fname +in_record true +type string +preserve_case true +tagged false +reader urword +longname file name +description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. + +block packages +name pname +in_record true +type string +tagged false +reader urword +optional true +longname user name for package +description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWT Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. + diff --git a/autotest/autotest/temp/dfn/gwt-oc.dfn b/autotest/autotest/temp/dfn/gwt-oc.dfn new file mode 100644 index 00000000..bae202eb --- /dev/null +++ b/autotest/autotest/temp/dfn/gwt-oc.dfn @@ -0,0 +1,318 @@ +# --------------------- gwt oc options --------------------- + +block options +name budget_filerecord +type record budget fileout budgetfile +shape +reader urword +tagged true +optional true +mf6internal budfilerec +longname +description + +block options +name budget +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget. + +block options +name fileout +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname file keyword +description keyword to specify that an output filename is expected next. + +block options +name budgetfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the output file to write budget information. + +block options +name budgetcsv_filerecord +type record budgetcsv fileout budgetcsvfile +shape +reader urword +tagged true +optional true +mf6internal budcsvfilerec +longname +description + +block options +name budgetcsv +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname budget keyword +description keyword to specify that record corresponds to the budget CSV. + +block options +name budgetcsvfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. + +block options +name concentration_filerecord +type record concentration fileout concentrationfile +shape +reader urword +tagged true +optional true +mf6internal concfilerec +longname +description + +block options +name concentration +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname concentration keyword +description keyword to specify that record corresponds to concentration. + +block options +name concentrationfile +type string +preserve_case true +shape +in_record true +reader urword +tagged false +optional false +longname file keyword +mf6internal concfile +description name of the output file to write conc information. + +block options +name concentrationprintrecord +type record concentration print_format formatrecord +shape +reader urword +optional true +mf6internal concprintrec +longname +description + +block options +name print_format +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to indicate that a print format follows +description keyword to specify format for printing to the listing file. + +block options +name formatrecord +type record columns width digits format +shape +in_record true +reader urword +tagged +optional false +longname +description + +block options +name columns +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of columns +description number of columns for writing data. + +block options +name width +type integer +shape +in_record true +reader urword +tagged true +optional +longname width for each number +description width for writing each number. + +block options +name digits +type integer +shape +in_record true +reader urword +tagged true +optional +longname number of digits +description number of digits to use for writing a number. + +block options +name format +type string +shape +in_record true +reader urword +tagged false +optional false +longname write format +description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. + + +# --------------------- gwt oc period --------------------- + +block period +name iper +type integer +block_variable true +in_record true +tagged false +shape +valid +reader urword +optional false +longname stress period number +description REPLACE iper {} + +block period +name saverecord +type record save rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name save +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be saved this stress period. + +block period +name printrecord +type record print rtype ocsetting +shape +reader urword +tagged false +optional true +longname +description + +block period +name print +type keyword +shape +in_record true +reader urword +tagged true +optional false +longname keyword to save +description keyword to indicate that information will be printed this stress period. + +block period +name rtype +type string +shape +in_record true +reader urword +tagged false +optional false +longname record type +description type of information to save or print. Can be BUDGET or CONCENTRATION. + +block period +name ocsetting +type keystring all first last frequency steps +shape +tagged false +in_record true +reader urword +longname +description specifies the steps for which the data will be saved. + +block period +name all +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for all time steps in period. + +block period +name first +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name last +type keyword +shape +in_record true +reader urword +longname +description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name frequency +type integer +shape +tagged true +in_record true +reader urword +longname +description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. + +block period +name steps +type integer +shape ( Field: + """Build a complete v1 Field dict for testing.""" + base: dict = { + "name": "test_field", + "type": "keyword", + "block": "options", + "default": None, + "longname": None, + "description": None, + "optional": False, + "developmode": False, + "shape": None, + "valid": None, + "netcdf": False, + "tagged": False, + } + base.update(kwargs) + return Field(**base) + + +def _dfn(**kwargs) -> Dfn: + """Build a minimal v1 Dfn dict for testing.""" + base: dict = { + "schema_version": "1", + "name": "test-dfn", + "parent": None, + "blocks": None, + "advanced": False, + "multi": False, + } + base.update(kwargs) + return Dfn(**base) + + +def test_map_field_preserves_base_attrs(): + field = _field( + name="save_flows", + type="keyword", + description="save calculated flows", + optional=True, + tagged=True, + longname="save flows flag", + ) + result = map_field(field) assert result["name"] == "save_flows" assert result["type"] == "keyword" + assert result["description"] == "save calculated flows" + assert result["optional"] is True + assert result["tagged"] is True + assert result["longname"] == "save flows flag" -def test_toml_safe_nested(): - """_toml_safe recurses into dicts and lists.""" - obj = {"a": [Version("2"), "plain"], "b": {"c": 99}} - result = _toml_safe(obj) - assert result["a"][0] == "2" - assert result["a"][1] == "plain" - assert result["b"]["c"] == 99 +def test_map_field_strips_v1_specific_attrs(): + field = _field(in_record=True, reader="urword") + result = map_field(field) + assert "in_record" not in result + assert "reader" not in result -def test_mapper_load_v1(dfn_name): - with ( - (DFN_DIR / "common.dfn").open() as common_file, - (DFN_DIR / f"{dfn_name}.dfn").open() as dfn_file, - ): - common = _load_common(common_file) - dfn = load(dfn_file, name=dfn_name, format="dfn", common=common) - assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) +def test_map_sets_schema_version(): + dfn = _dfn() + result = map_v1_1(dfn) + assert result["schema_version"] == "1.1" -def test_mapper_load_flat(): - dfns = load_flat(path=DFN_DIR) - for dfn in dfns.values(): - assert any(dfn.fields) == (dfn.name not in EMPTY_DFNS) +def test_map_preserves_metadata(): + dfn = _dfn(name="gwf-chd", parent="gwf-nam") + result = map_v1_1(dfn) + assert result["name"] == "gwf-chd" + assert result["schema_version"] == "1.1" -def test_dfn_to_plain_dict_version_coerced_and_none_excluded(): - """Version is coerced to str; None fields are excluded from output.""" - dfn = DfnSpec( - schema_version=Version("1.1"), - name="test-dfn", - parent=None, - blocks=None, - ) - d = _dfn_to_plain_dict(dfn) - assert d["schema_version"] == "1.1" - assert d["name"] == "test-dfn" - assert "parent" not in d - assert "blocks" not in d - - -def test_dfn_to_plain_dict_with_fieldbase_blocks(): - """FieldBase blocks are serialized via model_dump.""" - dfn = DfnSpec( - schema_version=Version("2"), - name="test-dfn", - blocks={ - "options": { - "nper": Integer(name="nper", description="number of periods"), - } - }, - ) - d = _dfn_to_plain_dict(dfn) - block = d["blocks"]["options"] - assert "nper" in block - assert block["nper"]["type"] == "integer" - assert block["nper"]["name"] == "nper" - - -def test_dfn_to_plain_dict_with_fieldv1_blocks(): - """FieldV1 blocks are serialized via dataclasses.asdict.""" - dfn = DfnSpec( - schema_version=Version("1"), - name="test-dfn", - blocks={ - "options": { - "save_flows": FieldV1(name="save_flows", type="keyword", block="options"), - } - }, - ) - d = _dfn_to_plain_dict(dfn) - block = d["blocks"]["options"] - assert "save_flows" in block - assert block["save_flows"]["name"] == "save_flows" - assert block["save_flows"]["type"] == "keyword" +def test_map_empty_blocks(): + dfn = _dfn(blocks=None) + result = map_v1_1(dfn) + assert result["blocks"] is None + + +def test_map_maps_block_fields(): + field = _field(name="maxbound", type="integer", block="dimensions") + dfn = _dfn(blocks={"dimensions": {"maxbound": field}}) + result = map_v1_1(dfn) + assert result["blocks"] is not None + assert "maxbound" in result["blocks"]["dimensions"] + assert result["blocks"]["dimensions"]["maxbound"]["name"] == "maxbound" + assert "in_record" not in result["blocks"]["dimensions"]["maxbound"] diff --git a/autotest/dfns/__init__.py b/autotest/dfns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/autotest/dfns/test_dfns.py b/autotest/dfns/test_dfns.py index 5937b9b8..6a41c4a4 100644 --- a/autotest/dfns/test_dfns.py +++ b/autotest/dfns/test_dfns.py @@ -1,18 +1,10 @@ -from __future__ import annotations - -from unittest.mock import patch - -import pytest -from packaging.version import Version - -from modflow_devtools.dfns import Dfns, LocalDfnRegistry +from modflow_devtools.dfns import Dfns from modflow_devtools.markers import requires_pkg -@pytest.mark.parametrize("schema_version", [None, Version("2")]) -def test_load(dfn_dir, schema_version): - spec = Dfns.load(dfn_dir, schema_version=schema_version) - assert spec.schema_version == Version("2") +def test_load(dfn_dir): + spec = Dfns.load(dfn_dir) + assert spec.schema_version == "2" assert spec.root is not None assert spec.root.name == "sim-nam" assert len(spec.components) > 100 @@ -26,60 +18,16 @@ def test_load(dfn_dir, schema_version): assert gwf_chd.name == "gwf-chd" assert gwf_chd.parent == "gwf-nam" - sim_children = spec.children_of("sim-nam") + sim_children = spec.children("sim-nam") assert "gwf-nam" in sim_children - gwf_children = spec.children_of("gwf-nam") + gwf_children = spec.children("gwf-nam") assert "gwf-chd" in gwf_children def test_load_empty_directory(function_tmpdir): - with pytest.raises(ValueError, match="No DFN files found"): - Dfns.load(function_tmpdir) - - -# ============================================================================= -# Convenience functions with path -# ============================================================================= - - -def test_get_dfn(dfn_dir): - from modflow_devtools.dfns import get_dfn - - dfn = get_dfn("gwf-chd", path=dfn_dir) - assert dfn.name == "gwf-chd" - assert dfn.parent == "gwf-nam" - - -def test_get_dfn_path(dfn_dir): - from modflow_devtools.dfns import get_dfn_path - - file_path = get_dfn_path("gwf-chd", path=dfn_dir) - assert file_path.exists() - assert file_path.name == "gwf-chd.dfn" - - -def test_list_components(dfn_dir): - from modflow_devtools.dfns import list_components - - components = list_components(path=dfn_dir) - assert len(components) > 100 - assert "gwf-chd" in components - - -# ============================================================================= -# Module-level functions -# ============================================================================= - - -@requires_pkg("boltons", "pydantic") -class TestModuleFunctions: - def test_list_components_local(dfn_dir): - registry = LocalDfnRegistry(path=dfn_dir) - components = registry.list_components() - assert len(components) > 100 - assert "gwf-chd" in components - assert "sim-nam" in components + spec = Dfns.load(function_tmpdir) + assert len(spec.components) == 0 # ============================================================================= @@ -101,45 +49,8 @@ def test_info_command(self): result = main(["info"]) assert result == 0 - def test_clean_command_no_cache(self, tmp_path): + def test_clean_command(self): from modflow_devtools.dfns.__main__ import main - with patch("modflow_devtools.dfns.__main__.get_cache_dir") as mock_cache_dir: - mock_cache_dir.return_value = tmp_path / "nonexistent" - result = main(["clean"]) - + result = main(["clean"]) assert result == 0 - - def test_sync_invalid_ref(self): - from modflow_devtools.dfns.__main__ import main - - result = main(["sync", "--ref", "not-a-version"]) - assert result == 1 - - -# ============================================================================= -# Autodiscovery workflow -# ============================================================================= - - -@requires_pkg("boltons", "pydantic") -def test_autodiscovery_workflow(dfn_dir): - from modflow_devtools.dfns import get_dfn, get_registry, list_components - - registry = get_registry(path=dfn_dir, ref="local") - - components = registry.list_components() - assert len(components) > 100 - - gwf_chd = registry.get_dfn("gwf-chd") - assert gwf_chd.name == "gwf-chd" - assert gwf_chd.blocks is not None - - chd_path = registry.get_dfn_path("gwf-chd") - assert chd_path.exists() - - components_list = list_components(path=dfn_dir) - assert "gwf-chd" in components_list - - dfn = get_dfn("gwf-wel", path=dfn_dir) - assert dfn.name == "gwf-wel" diff --git a/autotest/dfns/test_dfns_registry.py b/autotest/dfns/test_dfns_registry.py index 91b66823..d4bae0db 100644 --- a/autotest/dfns/test_dfns_registry.py +++ b/autotest/dfns/test_dfns_registry.py @@ -1,22 +1,23 @@ -import flaky +import json +from unittest.mock import MagicMock, patch + import pytest -from packaging.version import Version +from flaky import flaky from modflow_devtools.dfns.registry import LocalDfnRegistry, RemoteDfnRegistry def test_local_dfn_registry(dfn_dir): registry = LocalDfnRegistry(path=dfn_dir) - assert registry.source == "modflow6" assert registry.path == dfn_dir.resolve() spec = registry.spec - assert spec.schema_version == Version("2") + assert spec.schema_version == "2" assert len(spec.components) > 100 assert "gwf-chd" in spec.components assert "sim-nam" in spec.components - dfn = spec.components("gwf-chd") + dfn = spec.components["gwf-chd"] assert dfn.name == "gwf-chd" assert dfn.parent == "gwf-nam" @@ -29,29 +30,164 @@ def test_local_dfn_registry(dfn_dir): def test_remote_dfn_registry_init(): - registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - assert registry.source == "modflow6" - assert registry.ref == "6.6.0" + release_id = "MODFLOW-ORG/modflow6@6.6.0" + registry = RemoteDfnRegistry(release_id=release_id) + assert registry.release_id == release_id + + cache_dir = registry.cache_path + assert "modflow6" in str(cache_dir) + assert "6.6.0" in str(cache_dir) + + +def test_latest_tag_exact_tag(): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@6.6.0") + assert registry.latest_tag() == "6.6.0" + assert registry._latest is None # no network call, nothing cached + + +def test_latest_tag_resolves_via_api(): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + + mock_response = MagicMock() + mock_response.read.return_value = json.dumps({"tag_name": "v6.6.1"}).encode() + mock_response.__enter__ = lambda s: s + mock_response.__exit__ = MagicMock(return_value=False) - with pytest.raises(ValueError, match="not a valid release version"): - RemoteDfnRegistry(source="modflow6", ref="develop") + with patch( + "modflow_devtools.dfns.registry.urllib.request.urlopen", + return_value=mock_response, + ) as mock_open: + tag = registry.latest_tag() - with pytest.raises(ValueError, match="Unknown source"): - RemoteDfnRegistry(source="nonexistent", ref="6.6.0") + assert tag == "v6.6.1" + assert registry._latest == "v6.6.1" + mock_open.assert_called_once_with( + "https://api.github.com/repos/MODFLOW-ORG/modflow6/releases/latest" + ) - registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") - cache_dir = registry.cache_path() + +def test_latest_tag_cached(): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + + mock_response = MagicMock() + mock_response.read.return_value = json.dumps({"tag_name": "v6.6.1"}).encode() + mock_response.__enter__ = lambda s: s + mock_response.__exit__ = MagicMock(return_value=False) + + with patch( + "modflow_devtools.dfns.registry.urllib.request.urlopen", + return_value=mock_response, + ) as mock_open: + registry.latest_tag() + registry.latest_tag() + + mock_open.assert_called_once() # second call uses cached _latest + + +def test_cache_path_latest_uses_resolved_tag(): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + + mock_response = MagicMock() + mock_response.read.return_value = json.dumps({"tag_name": "v6.6.1"}).encode() + mock_response.__enter__ = lambda s: s + mock_response.__exit__ = MagicMock(return_value=False) + + with patch("modflow_devtools.dfns.registry.urllib.request.urlopen", return_value=mock_response): + cache_dir = registry.cache_path + + assert "latest" not in str(cache_dir) + assert "v6.6.1" in str(cache_dir) assert "modflow6" in str(cache_dir) - assert "6.6.0" in str(cache_dir) -@pytest.mark.skip(reason="Requires mf{version}_dfns.zip release asset on GitHub") +def test_cached_tag_exact_not_cached(tmp_path): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@6.6.0") + with patch.object( + type(registry), + "cache_path", + new_callable=lambda: property(lambda self: tmp_path / "empty"), + ): + assert registry.cached_tag() is None + + +def test_cached_tag_exact_cached(tmp_path): + cache_dir = tmp_path / "populated" + cache_dir.mkdir() + (cache_dir / "gwf-chd.toml").write_text("name = 'gwf-chd'") + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@6.6.0") + with patch.object( + type(registry), + "cache_path", + new_callable=lambda: property(lambda self: cache_dir), + ): + assert registry.cached_tag() == "6.6.0" + + +def test_cached_tag_latest_not_cached(tmp_path): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + with patch.object(RemoteDfnRegistry, "base_cache_path", return_value=tmp_path): + assert registry.cached_tag() is None + + +def test_cached_tag_latest_cached(tmp_path): + repo_cache = tmp_path / "MODFLOW-ORG" / "modflow6" + tag_dir = repo_cache / "6.7.0" + tag_dir.mkdir(parents=True) + (tag_dir / "gwf-chd.toml").write_text("name = 'gwf-chd'") + + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + with patch.object(RemoteDfnRegistry, "base_cache_path", return_value=tmp_path): + assert registry.cached_tag() == "6.7.0" + + +def test_cmd_info_no_network(tmp_path, capsys): + from modflow_devtools.dfns.__main__ import cmd_info + + repo_cache = tmp_path / "MODFLOW-ORG" / "modflow6" + tag_dir = repo_cache / "6.7.0" + tag_dir.mkdir(parents=True) + (tag_dir / "gwf-chd.toml").write_text("name = 'gwf-chd'") + + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + + with ( + patch.object( + RemoteDfnRegistry, + "load_default", + return_value={"MODFLOW-ORG/modflow6@latest": registry}, + ), + patch.object(RemoteDfnRegistry, "base_cache_path", return_value=tmp_path), + patch("modflow_devtools.dfns.registry.urllib.request.urlopen") as mock_open, + ): + import argparse + + result = cmd_info(argparse.Namespace()) + + mock_open.assert_not_called() + assert result == 0 + out = capsys.readouterr().out + assert "Cached" in out + assert "6.7.0" in out + assert "latest" in out + + +@pytest.mark.skip(reason="Requires network access to GitHub API") +@flaky(max_runs=3, min_passes=1) +def test_latest_tag_live(): + registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@latest") + tag = registry.latest_tag() + assert tag.startswith("v") or tag[0].isdigit() + assert registry._latest == tag + + +@pytest.mark.skip(reason="Requires dfns.zip release asset on GitHub") @flaky(max_runs=3, min_passes=1) def test_remote_dfn_registry_sync(): - registry = RemoteDfnRegistry(source=DFNS_SOURCE, ref=DFNS_VERSION) + release_id = "MODFLOW-ORG/modflow6@6.6.0" + registry = RemoteDfnRegistry(release_id=release_id) registry.sync(force=True) - cache_dir = registry.cache_path() + cache_dir = registry.cache_path assert cache_dir.exists() assert any(cache_dir.iterdir()) diff --git a/autotest/dfns/test_dfns_schema.py b/autotest/dfns/test_dfns_schema.py index d10ffd80..f6c4d003 100644 --- a/autotest/dfns/test_dfns_schema.py +++ b/autotest/dfns/test_dfns_schema.py @@ -1,16 +1,14 @@ -from __future__ import annotations - import ast import pytest -from modflow_devtools.dfn.v1_1 import Dfn, FieldV1 -from packaging.version import Version +from modflow_devtools.dfn import schema as v1 from modflow_devtools.dfns import Dfns from modflow_devtools.dfns.mapper import map as map_v2 from modflow_devtools.dfns.schema import ( Array, Block, + DimDef, Double, FieldBase, Integer, @@ -21,8 +19,6 @@ Record, Simulation, String, - _collect_explicit_dims, - _known_dims_for, _names_in_expr, _resolve_derived_dims, _validate_fk_fields, @@ -31,15 +27,49 @@ ) +def _v1_field(**kwargs) -> v1.Field: + base: dict = { + "name": "test_field", + "type": "keyword", + "block": "options", + "in_record": False, + "default": None, + "longname": None, + "description": None, + "optional": False, + "developmode": False, + "shape": None, + "valid": None, + "netcdf": False, + "tagged": False, + } + base.update(kwargs) + return v1.Field(**base) + + +def _v1_dfn(**kwargs) -> v1.Dfn: + base: dict = { + "schema_version": "1", + "name": "test-dfn", + "parent": None, + "blocks": None, + "advanced": False, + "multi": False, + "subcomponents": None, + } + base.update(kwargs) + return v1.Dfn(**base) + + def _dim_block(*names: str) -> Block: return Block( name="dimensions", - fields={n: Integer(name=n, dimension="component") for n in names}, + fields={n: Integer(name=n) for n in names}, ) -def _pkg(name: str, blocks=None, derived_dims=None, parent=None, **kw) -> Package: - return Package(name=name, blocks=blocks, derived_dims=derived_dims, parent=parent, **kw) +def _pkg(name: str, blocks=None, dims=None, parent=None, **kw) -> Package: + return Package(name=name, blocks=blocks, dims=dims, parent=parent, **kw) def test_fieldv2_from_dict(): @@ -81,65 +111,49 @@ def test_fieldv2_from_dict_roundtrip(): def test_map_v2(): - dfn = Dfn(schema_version=Version("2"), name="sim-nam") - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="sim-nam") + result = map_v2(dfn) assert isinstance(result, Simulation) - dfn = Dfn(schema_version=Version("2"), name="gwf-nam") - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="gwf-nam") + result = map_v2(dfn) assert isinstance(result, Model) - dfn = Dfn(schema_version=Version("2"), name="sln-ims") - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="sln-ims") + result = map_v2(dfn) assert isinstance(result, Package) assert result.subtype == "solution" - dfn = Dfn(schema_version=Version("2"), name="exg-gwfgwf") - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="exg-gwfgwf") + result = map_v2(dfn) assert isinstance(result, Package) assert result.subtype == "exchange" - dfn = Dfn(schema_version=Version("2"), name="utl-obs") - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="utl-obs") + result = map_v2(dfn) assert isinstance(result, Package) assert result.subtype == "utility" - dfn = Dfn(schema_version=Version("2"), name="gwf-sfr", advanced=True) - result = map_v2(dfn, "2") + dfn = _v1_dfn(name="gwf-sfr", advanced=True) + result = map_v2(dfn) assert isinstance(result, Package) assert result.subtype == "advanced" def test_map_v2_field_conversion(): - dfn = Dfn( - schema_version=Version("2"), - name="gwf-chd", - blocks={ - "options": { - "save_flows": Keyword(name="save_flows", description="save flows"), - } - }, - ) - result = map_v2(dfn, "2") - assert result.name == "gwf-chd" - assert result.blocks is not None - assert "save_flows" in result.blocks["options"].fields - - dfn = Dfn( - schema_version=Version("1"), + dfn = _v1_dfn( name="test-dfn", blocks={ "options": { - "save_flows": FieldV1( + "save_flows": _v1_field( name="save_flows", type="keyword", block="options", description="save calculated flows", tagged=True, in_record=False, - reader="urword", ), - "some_float": FieldV1( + "some_float": _v1_field( name="some_float", type="double precision", block="options", @@ -149,8 +163,7 @@ def test_map_v2_field_conversion(): }, ) - component = map_v2(dfn, schema_version="2") - assert component.schema_version == Version("2") + component = map_v2(dfn) assert component.blocks is not None assert "options" in component.blocks @@ -174,26 +187,25 @@ def test_map_v2_field_conversion(): def test_map_v2_period_block_conversion(): - dfn = Dfn( - schema_version=Version("1"), + dfn = _v1_dfn( name="test-pkg", blocks={ "period": { - "stress_period_data": FieldV1( + "stress_period_data": _v1_field( name="stress_period_data", type="recarray cellid q", block="period", description="stress period data", shape="(maxbound)", ), - "cellid": FieldV1( + "cellid": _v1_field( name="cellid", type="integer", block="period", shape="(ncelldim)", in_record=True, ), - "q": FieldV1( + "q": _v1_field( name="q", type="double precision", block="period", @@ -203,7 +215,7 @@ def test_map_v2_period_block_conversion(): }, ) - component = map_v2(dfn, schema_version="2") + component = map_v2(dfn) assert component.blocks is not None for block in component.blocks.values(): for f in block.fields.values(): @@ -220,32 +232,27 @@ def test_map_v2_period_block_conversion(): item_fields = spd.item.fields assert "cellid" in item_fields assert "q" in item_fields - q = item_fields["q"] - assert isinstance(q, Array) - assert q.dtype == "double" - assert q.shape == ["maxbound"] def test_map_v2_record_conversion(): """Record type with multiple scalar fields.""" - dfn = Dfn( - schema_version=Version("1"), + dfn = _v1_dfn( name="test-dfn", blocks={ "options": { - "auxrecord": FieldV1( + "auxrecord": _v1_field( name="auxrecord", type="record auxiliary auxname", block="options", in_record=False, ), - "auxiliary": FieldV1( + "auxiliary": _v1_field( name="auxiliary", type="keyword", block="options", in_record=True, ), - "auxname": FieldV1( + "auxname": _v1_field( name="auxname", type="string", block="options", @@ -255,7 +262,7 @@ def test_map_v2_record_conversion(): }, ) - component = map_v2(dfn, schema_version="2") + component = map_v2(dfn) auxrecord = component.blocks["options"].fields["auxrecord"] assert isinstance(auxrecord, Record) assert auxrecord.type == "record" @@ -268,30 +275,29 @@ def test_map_v2_record_conversion(): def test_keystring_type_conversion(): """Keystring (union) type conversion.""" - dfn = Dfn( - schema_version=Version("1"), + dfn = _v1_dfn( name="test-dfn", blocks={ "options": { - "obs_filerecord": FieldV1( + "obs_filerecord": _v1_field( name="obs_filerecord", type="record obs6 filein obs6_filename", block="options", tagged=True, ), - "obs6": FieldV1( + "obs6": _v1_field( name="obs6", type="keyword", block="options", in_record=True, ), - "filein": FieldV1( + "filein": _v1_field( name="filein", type="keyword", block="options", in_record=True, ), - "obs6_filename": FieldV1( + "obs6_filename": _v1_field( name="obs6_filename", type="string", block="options", @@ -302,7 +308,7 @@ def test_keystring_type_conversion(): }, ) - component = map_v2(dfn, schema_version="2") + component = map_v2(dfn) obs_rec = component.blocks["options"].fields["obs_filerecord"] assert isinstance(obs_rec, Record) assert obs_rec.type == "record" @@ -310,46 +316,34 @@ def test_keystring_type_conversion(): assert all(isinstance(child, FieldBase) for child in obs_rec.children.values()) -def test_to_component_variant_of(): - dfn = Dfn(schema_version=Version("2"), name="gwf-welg") - result = map_v2(dfn, "2") - assert isinstance(result, Package) - assert result.variant_of == "gwf-wel" - - dfn = Dfn(schema_version=Version("2"), name="gwf-rcha") - result = map_v2(dfn, "2") - assert isinstance(result, Package) - assert result.variant_of == "gwf-rch" - - -def test_collect_explicit_dims(): +def test_local_dims(): + # dims section populated → local_dims returns those names block = _dim_block("nlay", "nrow", "ncol") - pkg = _pkg("gwf-dis", blocks={"dimensions": block}) - assert _collect_explicit_dims(pkg) == {"nlay", "nrow", "ncol"} - - block = Block( - name="options", - fields={ - "maxbound": Integer(name="maxbound", dimension=False), - "nlay": Integer(name="nlay", dimension=True), - "name": String(name="name"), + pkg = Package( + name="gwf-dis", + blocks={"dimensions": block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), }, ) - pkg = _pkg("test", blocks={"options": block}) - assert _collect_explicit_dims(pkg) == {"nlay"} - - pkg = _pkg("test", blocks=None) - assert _collect_explicit_dims(pkg) == set() + spec = Dfns(components={"gwf-dis": pkg}) + assert spec.local_dims("gwf-dis") == {"nlay", "nrow", "ncol"} - b1 = Block(name="dimensions", fields={"nlay": Integer(name="nlay", dimension=True)}) - b2 = Block(name="griddata", fields={"ncol": Integer(name="ncol", dimension=True)}) - pkg = _pkg("test", blocks={"dimensions": b1, "griddata": b2}) - assert _collect_explicit_dims(pkg) == {"nlay", "ncol"} + # no dims section → empty + pkg2 = Package(name="gwf-chd", blocks=None, dims=None) + spec2 = Dfns(components={"gwf-chd": pkg2}) + assert spec2.local_dims("gwf-chd") == set() - b1 = Block(name="dimensions", fields={"nlay": Integer(name="nlay", dimension=True)}) - b2 = Block(name="griddata", fields={"ncol": Integer(name="ncol", dimension=True)}) - pkg = _pkg("test", blocks={"dimensions": b1, "griddata": b2}) - assert _collect_explicit_dims(pkg) == {"nlay", "ncol"} + # derived dims also included + pkg3 = Package( + name="test", + blocks=None, + dims={"nodes": DimDef(expr="42", scope="component")}, + ) + spec3 = Dfns(components={"test": pkg3}) + assert spec3.local_dims("test") == {"nodes"} def test_names_in_expr_simple_arithmetic(): @@ -431,21 +425,38 @@ def test_validate_sum_expr(): def test_resolve_derived_dims(): block = _dim_block("nlay", "nrow", "ncol") - pkg = _pkg("test", blocks={"dimensions": block}, derived_dims={"nodes": "nlay * nrow * ncol"}) + pkg = Package( + name="test", + blocks={"dimensions": block}, + dims={ + "nlay": DimDef(field="nlay", scope="component"), + "nrow": DimDef(field="nrow", scope="component"), + "ncol": DimDef(field="ncol", scope="component"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="component"), + }, + ) order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) assert order == ["nodes"] - block = _dim_block("nlay", "nrow", "ncol") - pkg = _pkg( - "test", + pkg = Package( + name="test", blocks={"dimensions": block}, - derived_dims={"nodes": "nlay * nrow * ncol", "nodouble": "nodes * 2"}, + dims={ + "nlay": DimDef(field="nlay", scope="component"), + "nrow": DimDef(field="nrow", scope="component"), + "ncol": DimDef(field="ncol", scope="component"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="component"), + "nodouble": DimDef(expr="nodes * 2", scope="component"), + }, ) order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) assert order.index("nodes") < order.index("nodouble") - # - pkg = _pkg("test", blocks=None, derived_dims={"derived": "nodes + 1"}) + pkg = Package( + name="test", + blocks=None, + dims={"derived": DimDef(expr="nodes + 1", scope="component")}, + ) order = _resolve_derived_dims(pkg, {"nodes"}) assert order == ["derived"] @@ -455,135 +466,210 @@ def test_resolve_derived_dims_sum_operand_allowed(): pkg = Package( name="test", blocks=pkg.blocks, - derived_dims={"total_conn": "sum(packagedata.nlakeconn)"}, + dims={"total_conn": DimDef(expr="sum(packagedata.nlakeconn)", scope="component")}, ) order = _resolve_derived_dims(pkg, set()) assert order == ["total_conn"] def test_resolve_derived_dims_no_derived_returns_empty(): - pkg = _pkg("test", blocks=None, derived_dims=None) + pkg = Package(name="test", blocks=None, dims=None) assert _resolve_derived_dims(pkg, set()) == [] def test_resolve_derived_dims_cycle_error(): - pkg = _pkg("test", blocks=None, derived_dims={"a": "b + 1", "b": "a + 1"}) - with pytest.raises(ValueError, match="Cycle in derived_dims"): + pkg = Package( + name="test", + blocks=None, + dims={ + "a": DimDef(expr="b + 1", scope="component"), + "b": DimDef(expr="a + 1", scope="component"), + }, + ) + with pytest.raises(ValueError, match="Cycle in"): _resolve_derived_dims(pkg, set()) def test_resolve_derived_dims_unknown_operand_error(): - pkg = _pkg("test", blocks=None, derived_dims={"nodes": "mystery_dim * 2"}) + pkg = Package( + name="test", + blocks=None, + dims={"nodes": DimDef(expr="mystery_dim * 2", scope="component")}, + ) with pytest.raises(ValueError, match="not a known dimension"): _resolve_derived_dims(pkg, set()) def test_resolve_derived_dims_invalid_expression_error(): - pkg = _pkg("test", blocks=None, derived_dims={"nodes": "nlay * ("}) - with pytest.raises(ValueError, match="Invalid derived_dims"): + pkg = Package( + name="test", + blocks=None, + dims={"nodes": DimDef(expr="nlay * (", scope="component")}, + ) + with pytest.raises(ValueError, match="Invalid"): _resolve_derived_dims(pkg, set()) def test_dfnspec_construction_validates_dims(): block = _dim_block("nlay", "nrow", "ncol") - pkg = _pkg( - "gwf-dis", + pkg = Package( + name="gwf-dis", blocks={"dimensions": block}, - derived_dims={"nodes": "nlay * nrow * ncol"}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, ) spec = Dfns(components={"gwf-dis": pkg}) assert "gwf-dis" in spec.components def test_dfnspec_construction_cycle_raises(): - pkg = _pkg("bad", blocks=None, derived_dims={"a": "b + 1", "b": "a + 1"}) - with pytest.raises(ValueError, match="Cycle in derived_dims"): + pkg = Package( + name="bad", + blocks=None, + dims={ + "a": DimDef(expr="b + 1", scope="component"), + "b": DimDef(expr="a + 1", scope="component"), + }, + ) + with pytest.raises(ValueError, match="Cycle in"): Dfns(components={"bad": pkg}) def test_dfnspec_construction_unknown_operand_raises(): - pkg = _pkg("bad", blocks=None, derived_dims={"nodes": "ghost_dim * 2"}) + pkg = Package( + name="bad", + blocks=None, + dims={"nodes": DimDef(expr="ghost_dim * 2", scope="component")}, + ) with pytest.raises(ValueError, match="not a known dimension"): Dfns(components={"bad": pkg}) -def test_dfnspec_no_derived_dims_constructs_fine(): - pkg = _pkg("gwf-chd", blocks=None, derived_dims=None) +def test_dfnspec_no_dims_constructs_fine(): + pkg = Package(name="gwf-chd", blocks=None, dims=None) spec = Dfns(components={"gwf-chd": pkg}) assert "gwf-chd" in spec.components # ============================================================================= -# dfns.schema.v2 — DfnSpec.explicit_dims_for +# dfns.schema.v2 — DfnSpec.local_dims # ============================================================================= -def test_dfnspec_explicit_dims_for(): +def test_dfnspec_local_dims(): block = _dim_block("nlay", "nrow", "ncol") - pkg = _pkg("gwf-dis", blocks={"dimensions": block}) + pkg = Package( + name="gwf-dis", + blocks={"dimensions": block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + }, + ) spec = Dfns(components={"gwf-dis": pkg}) - assert spec.explicit_dims_for("gwf-dis") == {"nlay", "nrow", "ncol"} + assert spec.local_dims("gwf-dis") == {"nlay", "nrow", "ncol"} -def test_dfnspec_explicit_dims_for_empty(): - pkg = _pkg("gwf-chd", blocks=None) +def test_dfnspec_local_dims_empty(): + pkg = Package(name="gwf-chd", blocks=None, dims=None) spec = Dfns(components={"gwf-chd": pkg}) - assert spec.explicit_dims_for("gwf-chd") == set() + assert spec.local_dims("gwf-chd") == set() -def test_dfnspec_grid_dims_for_includes_dis_dims(): +def test_dfnspec_inherited_dims_includes_dis_dims(): dis_block = _dim_block("nlay", "nrow", "ncol") - dis = _pkg("gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, + ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) - grid_dims = spec.grid_dims_for("gwf-chd") - assert "nlay" in grid_dims - assert "nrow" in grid_dims - assert "ncol" in grid_dims - assert "nodes" in grid_dims # from GRID_DIM_NAMESPACE + inherited = spec.inherited_dims("gwf-chd") + assert "nlay" in inherited + assert "nrow" in inherited + assert "ncol" in inherited + assert "nodes" in inherited # derived dim from gwf-dis, model-type scoped to "gwf" -def test_dfnspec_grid_dims_for_disv(): +def test_dfnspec_inherited_dims_disv(): disv_block = _dim_block("nlay", "ncpl") - disv = _pkg("gwf-disv", parent="gwf-nam", blocks={"dimensions": disv_block}) + disv = Package( + name="gwf-disv", + parent="gwf-nam", + blocks={"dimensions": disv_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "ncpl": DimDef(field="ncpl", scope="gwf"), + }, + ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-disv": disv, "gwf-chd": chd}) - grid_dims = spec.grid_dims_for("gwf-chd") - assert "nlay" in grid_dims - assert "ncpl" in grid_dims + inherited = spec.inherited_dims("gwf-chd") + assert "nlay" in inherited + assert "ncpl" in inherited -def test_dfnspec_grid_dims_for_disu(): +def test_dfnspec_inherited_dims_disu(): disu_block = _dim_block("nodes", "nja") - disu = _pkg("gwf-disu", parent="gwf-nam", blocks={"dimensions": disu_block}) + disu = Package( + name="gwf-disu", + parent="gwf-nam", + blocks={"dimensions": disu_block}, + dims={ + "nodes": DimDef(field="nodes", scope="gwf"), + "nja": DimDef(field="nja", scope="gwf"), + }, + ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-disu": disu, "gwf-chd": chd}) - grid_dims = spec.grid_dims_for("gwf-chd") - assert "nodes" in grid_dims - assert "nja" in grid_dims + inherited = spec.inherited_dims("gwf-chd") + assert "nodes" in inherited + assert "nja" in inherited -def test_dfnspec_grid_dims_for_non_dis_siblings_excluded(): +def test_dfnspec_inherited_dims_excludes_own(): + """Own dims appear in local_dims but not in inherited_dims.""" dis_block = _dim_block("nlay", "nrow", "ncol") - dis = _pkg("gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) - - other_block = Block( - name="dimensions", - fields={"secret_dim": Integer(name="secret_dim", dimension=True)}, + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + }, + ) + chd = Package( + name="gwf-chd", + parent="gwf-nam", + blocks={"dimensions": _dim_block("secret_dim")}, + dims={"secret_dim": DimDef(field="secret_dim", scope="gwf")}, ) - other = _pkg("gwf-chd", parent="gwf-nam", blocks={"dimensions": other_block}) gwf = Model(name="gwf-nam", blocks=None) - spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": other}) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) - grid_dims = spec.grid_dims_for("gwf-chd") - assert "nlay" in grid_dims - assert "secret_dim" not in grid_dims + inherited = spec.inherited_dims("gwf-chd") + assert "nlay" in inherited + assert "secret_dim" not in inherited # own dim: not in inherited_dims # ============================================================================= @@ -622,15 +708,15 @@ def test_dfnspec_components_contains(): def test_dfnspec_schema_version_from_component(): - pkg = Package(name="gwf-chd", schema_version=Version("2")) + pkg = Package(name="gwf-chd", schema_version="2") spec = Dfns(components={"gwf-chd": pkg}) - assert spec.schema_version == Version("2") + assert spec.schema_version == "2" def test_dfnspec_schema_version_default(): pkg = _pkg("gwf-chd") spec = Dfns(components={"gwf-chd": pkg}) - assert spec.schema_version == Version("2") + assert spec.schema_version == "2" # ============================================================================= @@ -644,18 +730,18 @@ def test_dfnspec_children_of(): rch = _pkg("gwf-rch", parent="gwf-nam") sim = Simulation(name="sim-nam", blocks=None) spec = Dfns(components={"sim-nam": sim, "gwf-nam": gwf, "gwf-chd": chd, "gwf-rch": rch}) - children = spec.children_of("gwf-nam") + children = spec.children("gwf-nam") assert set(children) == {"gwf-chd", "gwf-rch"} def test_dfnspec_children_of_empty(): pkg = _pkg("gwf-chd", parent="gwf-nam") spec = Dfns(components={"gwf-chd": pkg}) - assert spec.children_of("gwf-chd") == {} + assert spec.children("gwf-chd") == {} # ============================================================================= -# dfns.schema.v2 — _known_dims_for +# dfns.schema.v2 — Dfns.dims # ============================================================================= @@ -663,7 +749,17 @@ def _dis_spec() -> Dfns: """A minimal gwf-dis + gwf-nam DfnSpec used as shared fixture scaffolding.""" dis_block = _dim_block("nlay", "nrow", "ncol") gwf = Model(name="gwf-nam", blocks=None) - dis = Package(name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, + ) return Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -688,34 +784,39 @@ def _lake_spec(period_item: Record) -> Dfns: return Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) -def test_known_dims_includes_explicit(): +def test_dims_includes_own(): spec = _dis_spec() - known = _known_dims_for(spec, "gwf-dis") - assert {"nlay", "nrow", "ncol"} <= known + known = spec.dims("gwf-dis") + assert {"nlay", "nrow", "ncol", "nodes"} <= known -def test_known_dims_includes_derived(): +def test_dims_includes_derived(): dis_block = _dim_block("nlay", "nrow", "ncol") gwf = Model(name="gwf-nam", blocks=None) dis = Package( name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}, - derived_dims={"nodes": "nlay * nrow * ncol"}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, ) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) - known = _known_dims_for(spec, "gwf-dis") + known = spec.dims("gwf-dis") assert "nodes" in known -def test_known_dims_includes_grid_dims(): +def test_dims_includes_model_type_scoped(): + """A gwf-chd component inherits model-type-scoped dims from gwf-dis.""" spec = _dis_spec() - # gwf-chd has no local dims but inherits grid dims via gwf-dis sibling chd = _pkg("gwf-chd", parent="gwf-nam") spec2 = Dfns(components=dict(spec.components) | {"gwf-chd": chd}) - known = _known_dims_for(spec2, "gwf-chd") - assert "nodes" in known # GRID_DIM_NAMESPACE - assert "nlay" in known # from gwf-dis (sibling dis package) + known = spec2.dims("gwf-chd") + assert "nodes" in known # derived dim from gwf-dis, scope="gwf" + assert "nlay" in known # field-backed dim from gwf-dis, scope="gwf" # ============================================================================= @@ -725,11 +826,14 @@ def test_known_dims_includes_grid_dims(): def _make_ctx(dim_names: set[str], derived: dict | None = None): """Return (array, component, known_dims) for shape element tests.""" - dis_block = _dim_block(*dim_names) - pkg = _pkg("test", blocks={"dimensions": dis_block}, derived_dims=derived) + dims: dict[str, DimDef] = {n: DimDef(field=n, scope="component") for n in dim_names} + if derived: + dims.update({n: DimDef(expr=e, scope="component") for n, e in derived.items()}) + blocks = {"dimensions": _dim_block(*dim_names)} if dim_names else None + pkg = Package(name="test", blocks=blocks, dims=dims or None) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "test": pkg}) - known = _known_dims_for(spec, "test") + known = spec.dims("test") arr = Array(name="arr", dtype="double", shape=[]) return arr, pkg, known @@ -739,10 +843,19 @@ def test_shape_element_valid_explicit_dim(): _validate_shape_element("nlay", arr, pkg, None, known) # no error -def test_shape_element_valid_grid_dim(): - arr, pkg, known = _make_ctx(set()) - # "nodes" is always in GRID_DIM_NAMESPACE → known - _validate_shape_element("nodes", arr, pkg, None, known) +def test_shape_element_valid_inherited_dim(): + """A dim declared in a sibling component (model-type scoped) is valid.""" + dis = Package( + name="gwf-dis", + blocks=None, + dims={"nodes": DimDef(expr="42", scope="gwf")}, + ) + test_pkg = Package(name="gwf-test", blocks=None) + gwf = Model(name="gwf-nam", blocks=None) + spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-test": test_pkg}) + known = spec.dims("gwf-test") + arr = Array(name="arr", dtype="double", shape=[]) + _validate_shape_element("nodes", arr, test_pkg, None, known) def test_shape_element_valid_derived_dim(): @@ -798,7 +911,7 @@ def _lookup_ctx(): ) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) - known = _known_dims_for(spec, "gwf-lak") + known = spec.dims("gwf-lak") return arr, enc_record, lak, known @@ -837,7 +950,7 @@ def test_shape_element_lookup_non_integer_column_raises(): lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) - known = _known_dims_for(spec, "gwf-lak") + known = spec.dims("gwf-lak") with pytest.raises(ValueError, match="must be Integer"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) @@ -860,7 +973,7 @@ def test_shape_element_lookup_fk_not_set_raises(): lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) - known = _known_dims_for(spec, "gwf-lak") + known = spec.dims("gwf-lak") with pytest.raises(ValueError, match=r"\.fk is not set"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) @@ -877,7 +990,7 @@ def test_shape_element_lookup_fk_block_mismatch_raises(): lak = Package(name="gwf-lak", parent="gwf-nam", blocks={"packagedata": pkg_block}) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-lak": lak}) - known = _known_dims_for(spec, "gwf-lak") + known = spec.dims("gwf-lak") with pytest.raises(ValueError, match="does not reference block"): _validate_shape_element("packagedata.nlakeconn(lakeno)", arr, lak, enc, known) @@ -895,6 +1008,11 @@ def test_dfnspec_valid_top_level_array_shape(): name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + }, ) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -910,6 +1028,11 @@ def test_dfnspec_valid_array_in_record(): name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block, "options": opt_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + }, ) gwf = Model(name="gwf-nam", blocks=None) Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -945,6 +1068,11 @@ def test_dfnspec_invalid_array_shape_raises(): name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + }, ) gwf = Model(name="gwf-nam", blocks=None) with pytest.raises(ValueError, match="does not resolve"): @@ -959,16 +1087,31 @@ def test_dfnspec_array_shape_resolves_via_derived_dim(): name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, - derived_dims={"nodes": "nlay * nrow * ncol"}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, ) gwf = Model(name="gwf-nam", blocks=None) Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) def test_dfnspec_array_shape_resolves_via_sibling_dis(): - """An array in gwf-chd can reference nlay from sibling gwf-dis.""" + """An array in gwf-chd can reference nlay and nodes from sibling gwf-dis.""" dis_block = _dim_block("nlay", "nrow", "ncol") - dis = Package(name="gwf-dis", parent="gwf-nam", blocks={"dimensions": dis_block}) + dis = Package( + name="gwf-dis", + parent="gwf-nam", + blocks={"dimensions": dis_block}, + dims={ + "nlay": DimDef(field="nlay", scope="gwf"), + "nrow": DimDef(field="nrow", scope="gwf"), + "ncol": DimDef(field="ncol", scope="gwf"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + }, + ) chd_arr = Array(name="head", dtype="double", shape=["nlay", "nodes"]) chd_block = Block(name="period", fields={"head": chd_arr}) chd = Package(name="gwf-chd", parent="gwf-nam", blocks={"period": chd_block}) @@ -1134,17 +1277,17 @@ def test_rightmost_inline_string_array_empty_shape_valid(): def test_dfnspec_schema_version_consistency_raises(): - pkg1 = Package(name="gwf-chd", schema_version=Version("2")) - pkg2 = Package(name="gwf-wel", schema_version=Version("3")) + pkg1 = Package(name="gwf-chd", schema_version="2") + pkg2 = Package(name="gwf-wel", schema_version="3") with pytest.raises(ValueError, match="schema_version"): Dfns(components={"gwf-chd": pkg1, "gwf-wel": pkg2}) def test_dfnspec_schema_version_consistency_null_ignored(): - pkg1 = Package(name="gwf-chd", schema_version=Version("2")) + pkg1 = Package(name="gwf-chd", schema_version="2") pkg2 = Package(name="gwf-wel", schema_version=None) spec = Dfns(components={"gwf-chd": pkg1, "gwf-wel": pkg2}) - assert spec.schema_version == Version("2") + assert spec.schema_version == "2" # ============================================================================= diff --git a/autotest/dfns/test_mapper.py b/autotest/dfns/test_mapper.py index 2146d8d6..b44af973 100644 --- a/autotest/dfns/test_mapper.py +++ b/autotest/dfns/test_mapper.py @@ -1,10 +1,211 @@ -import tomli -from pydantic import TypeAdapter +import pytest -from modflow_devtools.dfns.schema import Component +from modflow_devtools.dfn import schema as v1 +from modflow_devtools.dfns.mapper import map as map_v2 +from modflow_devtools.dfns.schema import ( + Double, + Keyword, + List, + Model, + Package, + Record, + Simulation, + String, +) -def test_convert_v2(toml_v2_name): - with (TOML_V2_DIR / f"{toml_v2_name}.toml").open("rb") as f: - data = tomli.load(f) - assert TypeAdapter(Component).validate_python(data).name == toml_v2_name +def _v1_field(**kwargs) -> v1.Field: + base: dict = { + "name": "test_field", + "type": "keyword", + "block": "options", + "in_record": False, + "default": None, + "longname": None, + "description": None, + "optional": False, + "developmode": False, + "shape": None, + "valid": None, + "netcdf": False, + "tagged": False, + } + base.update(kwargs) + return v1.Field(**base) + + +def _v1_dfn(**kwargs) -> v1.Dfn: + base: dict = { + "schema_version": "1", + "name": "test-dfn", + "parent": None, + "blocks": None, + "advanced": False, + "multi": False, + "subcomponents": None, + } + base.update(kwargs) + return v1.Dfn(**base) + + +def test_map_sim_nam_returns_simulation(): + dfn = _v1_dfn(name="sim-nam") + result = map_v2(dfn) + assert isinstance(result, Simulation) + + +def test_map_model_returns_model(): + dfn = _v1_dfn(name="gwf-nam") + result = map_v2(dfn) + assert isinstance(result, Model) + + dfn = _v1_dfn(name="gwt-nam") + result = map_v2(dfn) + assert isinstance(result, Model) + + +def test_map_solution_package(): + dfn = _v1_dfn(name="sln-ims") + result = map_v2(dfn) + assert isinstance(result, Package) + assert result.subtype == "solution" + + +def test_map_exchange_package(): + dfn = _v1_dfn(name="exg-gwfgwf") + result = map_v2(dfn) + assert isinstance(result, Package) + assert result.subtype == "exchange" + + +def test_map_utility_package(): + dfn = _v1_dfn(name="utl-obs") + result = map_v2(dfn) + assert isinstance(result, Package) + assert result.subtype == "utility" + + +def test_map_advanced_package(): + dfn = _v1_dfn(name="gwf-sfr", advanced=True) + result = map_v2(dfn) + assert isinstance(result, Package) + assert result.subtype == "advanced" + + +def test_map_wrong_schema_version_raises(): + dfn = _v1_dfn(schema_version="2") + with pytest.raises(ValueError, match="schema version"): + map_v2(dfn) + + +def test_map_keyword_field_conversion(): + dfn = _v1_dfn( + name="gwf-chd", + blocks={ + "options": { + "save_flows": _v1_field( + name="save_flows", + type="keyword", + description="save calculated flows", + tagged=True, + in_record=False, + ), + } + }, + ) + component = map_v2(dfn) + assert component.blocks is not None + options = component.blocks["options"].fields + assert "save_flows" in options + field = options["save_flows"] + assert isinstance(field, Keyword) + assert field.name == "save_flows" + assert field.description == "save calculated flows" + + +def test_map_double_precision_field_conversion(): + dfn = _v1_dfn( + name="gwf-chd", + blocks={ + "options": { + "some_float": _v1_field( + name="some_float", + type="double precision", + description="a floating point value", + ), + } + }, + ) + component = map_v2(dfn) + options = component.blocks["options"].fields + field = options["some_float"] + assert isinstance(field, Double) + assert field.type == "double" + + +def test_map_record_conversion(): + dfn = _v1_dfn( + name="test-dfn", + blocks={ + "options": { + "auxrecord": _v1_field( + name="auxrecord", + type="record auxiliary auxname", + in_record=False, + ), + "auxiliary": _v1_field( + name="auxiliary", + type="keyword", + in_record=True, + ), + "auxname": _v1_field( + name="auxname", + type="string", + in_record=True, + ), + } + }, + ) + component = map_v2(dfn) + auxrecord = component.blocks["options"].fields["auxrecord"] + assert isinstance(auxrecord, Record) + assert "auxiliary" in auxrecord.children + assert "auxname" in auxrecord.children + assert isinstance(auxrecord.children["auxiliary"], Keyword) + assert isinstance(auxrecord.children["auxname"], String) + + +def test_map_recarray_conversion(): + dfn = _v1_dfn( + name="test-pkg", + blocks={ + "period": { + "stress_period_data": _v1_field( + name="stress_period_data", + type="recarray cellid q", + block="period", + shape="(maxbound)", + ), + "cellid": _v1_field( + name="cellid", + type="integer", + block="period", + shape="(ncelldim)", + in_record=True, + ), + "q": _v1_field( + name="q", + type="double precision", + block="period", + in_record=True, + ), + } + }, + ) + component = map_v2(dfn) + period_fields = component.blocks["period"].fields + spd = period_fields["stress_period_data"] + assert isinstance(spd, List) + assert isinstance(spd.item, Record) + assert "cellid" in spd.item.fields + assert "q" in spd.item.fields diff --git a/autotest/test_models.py b/autotest/test_models.py index 54658773..6b8e12ca 100644 --- a/autotest/test_models.py +++ b/autotest/test_models.py @@ -17,8 +17,8 @@ DiscoveredModelRegistry, ModelRegistry, ModelRegistryDiscoveryError, - ModelSource, - ModelSources, + ModelSourceConfig, + ModelSourceRepo, get_user_config_path, ) @@ -33,19 +33,19 @@ class TestBootstrap: def test_load_bootstrap(self): """Test loading the bootstrap file.""" - bootstrap = ModelSources.load() - assert isinstance(bootstrap, ModelSources) + bootstrap = ModelSourceConfig.load() + assert isinstance(bootstrap, ModelSourceConfig) assert len(bootstrap.sources) > 0 def test_bootstrap_has_testmodels(self): """Test that testmodels is configured.""" - bootstrap = ModelSources.load() + bootstrap = ModelSourceConfig.load() assert TEST_MODELS_SOURCE in bootstrap.sources def test_bootstrap_testmodels_config(self): """Test testmodels configuration in bundled config (without user overlay).""" bundled_path = Path(__file__).parent.parent / "modflow_devtools" / "models" / "models.toml" - bootstrap = ModelSources.load(bootstrap_path=bundled_path) + bootstrap = ModelSourceConfig.load(bootstrap_path=bundled_path) testmodels = bootstrap.sources[TEST_MODELS_SOURCE] assert "MODFLOW-ORG/modflow6-testmodels" in testmodels.repo @@ -53,7 +53,7 @@ def test_bootstrap_testmodels_config(self): def test_bootstrap_source_has_name(self): """Test that bootstrap sources have name injected.""" - bootstrap = ModelSources.load() + bootstrap = ModelSourceConfig.load() for key, source in bootstrap.sources.items(): assert source.name is not None # If no explicit name override, name should equal key @@ -72,23 +72,25 @@ def test_get_user_config_path(self): def test_merge_bootstrap(self): """Test merging bundled and user bootstrap configs.""" # Create bundled config - bundled = ModelSources( + bundled = ModelSourceConfig( sources={ - "source1": ModelSource(repo="org/repo1", name="source1", refs=["main"]), - "source2": ModelSource(repo="org/repo2", name="source2", refs=["develop"]), + "source1": ModelSourceRepo(repo="org/repo1", name="source1", refs=["main"]), + "source2": ModelSourceRepo(repo="org/repo2", name="source2", refs=["develop"]), } ) # Create user config that overrides source1 and adds source3 - user = ModelSources( + user = ModelSourceConfig( sources={ - "source1": ModelSource(repo="user/custom-repo1", name="source1", refs=["feature"]), - "source3": ModelSource(repo="user/repo3", name="source3", refs=["master"]), + "source1": ModelSourceRepo( + repo="user/custom-repo1", name="source1", refs=["feature"] + ), + "source3": ModelSourceRepo(repo="user/repo3", name="source3", refs=["master"]), } ) # Merge - merged = ModelSources.merge(bundled, user) + merged = ModelSourceConfig.merge(bundled, user) # Check that user source1 overrode bundled source1 assert merged.sources["source1"].repo == "user/custom-repo1" @@ -119,7 +121,7 @@ def test_load_bootstrap_with_user_config(self, tmp_path): ) # Load bootstrap with user config path specified - bootstrap = ModelSources.load(user_config_path=user_config) + bootstrap = ModelSourceConfig.load(user_config_path=user_config) # Check that user config was merged assert "custom-models" in bootstrap.sources @@ -152,7 +154,7 @@ def test_load_bootstrap_explicit_path_no_overlay(self, tmp_path): ) # Load with explicit path only (no user_config_path) - bootstrap = ModelSources.load(explicit_config) + bootstrap = ModelSourceConfig.load(explicit_config) # Should only have explicit source, not user source assert "explicit-source" in bootstrap.sources @@ -181,7 +183,9 @@ def test_load_bootstrap_explicit_path_with_overlay(self, tmp_path): ) # Load with both explicit paths - bootstrap = ModelSources.load(bootstrap_path=explicit_config, user_config_path=user_config) + bootstrap = ModelSourceConfig.load( + bootstrap_path=explicit_config, user_config_path=user_config + ) # Should have both sources assert "explicit-source" in bootstrap.sources @@ -195,7 +199,7 @@ class TestBootstrapSourceMethods: def test_source_has_sync_method(self): """Test that ModelSourceRepo has sync method.""" - bootstrap = ModelSources.load() + bootstrap = ModelSourceConfig.load() source = bootstrap.sources[TEST_MODELS_SOURCE] assert hasattr(source, "sync") assert callable(source.sync) @@ -232,7 +236,7 @@ class TestDiscovery: def test_discover_registry(self): """Test discovering registry for test repo.""" # Use test repo/ref from environment - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -249,7 +253,7 @@ def test_discover_registry(self): @flaky(max_runs=3, min_passes=1) def test_discover_registry_nonexistent_ref(self): """Test that discovery fails gracefully for nonexistent ref.""" - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=["nonexistent-branch-12345"], @@ -268,11 +272,10 @@ def test_sync_single_source_single_ref(self): """Test syncing a single source/ref.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], - verbose=True, ) result = source.sync(ref=TEST_MODELS_REF, verbose=True) @@ -286,7 +289,7 @@ def test_sync_creates_cache(self): _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) assert not _DEFAULT_CACHE.has(TEST_MODELS_SOURCE_NAME, TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -299,7 +302,7 @@ def test_sync_skip_cached(self): """Test that sync skips already-cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -319,7 +322,7 @@ def test_sync_force(self): """Test that force flag re-syncs cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -340,7 +343,7 @@ def test_sync_via_source_method(self): _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) # Create source with test repo override - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -357,7 +360,7 @@ def test_source_is_synced_method(self): """Test ModelSourceRepo.is_synced() method.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -372,7 +375,7 @@ def test_source_list_synced_refs_method(self): """Test ModelSourceRepo.list_synced_refs() method.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -391,7 +394,7 @@ class TestRegistry: def synced_registry(self): """Fixture that syncs and loads a registry once for all tests.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -459,7 +462,7 @@ def test_cli_list_empty(self, capsys): def test_cli_list_with_cache(self, capsys): """Test 'list' command with cached registries.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -486,7 +489,7 @@ def test_cli_clear(self, capsys): """Test 'clear' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -515,7 +518,7 @@ def test_cli_copy(self, tmp_path): """Test 'copy' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -552,7 +555,7 @@ def test_cli_copy_nonexistent_model(self, tmp_path, capsys): """Test 'copy' command with nonexistent model.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -585,7 +588,7 @@ def test_cli_cp_alias(self, tmp_path): """Test 'cp' alias for 'copy' command.""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -623,7 +626,7 @@ def test_python_cp_alias(self, tmp_path): """Test Python API cp() alias for copy_to().""" # Sync a registry first _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -662,7 +665,7 @@ def test_full_workflow(self): """Test complete workflow: discover -> cache -> load.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], @@ -685,7 +688,7 @@ def test_sync_and_list_models(self): """Test syncing and listing available models.""" _DEFAULT_CACHE.clear(source=TEST_MODELS_SOURCE_NAME, ref=TEST_MODELS_REF) - source = ModelSource( + source = ModelSourceRepo( repo=TEST_MODELS_REPO, name=TEST_MODELS_SOURCE_NAME, refs=[TEST_MODELS_REF], diff --git a/docs/md/dev/dfns.md b/docs/md/dev/dfns.md index 4b964dfd..e6fd6ba3 100644 --- a/docs/md/dev/dfns.md +++ b/docs/md/dev/dfns.md @@ -1,930 +1,175 @@ -# DFNs API Design - -This document describes the design of the DFNs (Definition Files) API ([GitHub issue #262](https://github.com/MODFLOW-ORG/modflow-devtools/issues/262)). It is intended to be developer-facing, not user-facing, though users may also find it informative. - -This is a living document which will be updated as development proceeds. - - - - - -- [Background](#background) -- [Objective](#objective) -- [Overview](#overview) -- [Architecture](#architecture) - - [Bootstrap file](#bootstrap-file) - - [Bootstrap file contents](#bootstrap-file-contents) - - [Sample bootstrap file](#sample-bootstrap-file) - - [DFN spec and registry files](#dfn-spec-and-registry-files) - - [Registry file format](#registry-file-format) - - [Sample files](#sample-files) - - [Registry discovery](#registry-discovery) - - [Discovery modes](#discovery-modes) - - [Registry discovery procedure](#registry-discovery-procedure) - - [Registry/DFN caching](#registrydfn-caching) - - [Registry synchronization](#registry-synchronization) - - [Manual sync](#manual-sync) - - [Automatic sync](#automatic-sync) - - [Source repository integration](#source-repository-integration) - - [DFN addressing](#dfn-addressing) - - [Registry classes](#registry-classes) - - [DfnRegistry (base class)](#dfnregistry-base-class) - - [RemoteDfnRegistry](#remotedfnregistry) - - [LocalDfnRegistry](#localdfnregistry) - - [Module-level API](#module-level-api) -- [Schema Versioning](#schema-versioning) - - [Separating format from schema](#separating-format-from-schema) - - [Schema evolution](#schema-evolution) - - [Tentative v2 schema design](#tentative-v2-schema-design) -- [Component Hierarchy](#component-hierarchy) -- [Schema version support](#schema-version-support) -- [Implementation Dependencies](#implementation-dependencies) - - [Completed work](#completed-work) - - [Core components](#core-components) - - [MODFLOW 6 repository integration](#modflow-6-repository-integration) - - [Testing and documentation](#testing-and-documentation) -- [Relationship to Models and Programs APIs](#relationship-to-models-and-programs-apis) -- [Design Decisions](#design-decisions) - - [Use Pooch for fetching](#use-pooch-for-fetching) - - [Use Pydantic for schema validation](#use-pydantic-for-schema-validation) - - [Schema versioning strategy](#schema-versioning-strategy) - - [Future enhancements](#future-enhancements) - - +# DFNs API -## Background - -The `modflow_devtools.dfns` module currently provides utilities for parsing and working with MODFLOW 6 definition files. Significant work already completed includes: - -- Object models for DFN components (`Dfn`, `Block`, `Field` classes) -- Schema definitions for both v1 (legacy) and v2 (in development) -- Parsers for the old DFN format -- Schema mapping capabilities including utilities for converting between flat and hierarchical component representations -- A `fetch_dfns()` function for manually downloading DFN files from the MODFLOW 6 repository -- Validation tools - -However, there is currently no registry-based API for: -- Automatically discovering and synchronizing DFN files from remote sources -- Managing multiple versions of definition files simultaneously -- Caching definition files locally for offline use - -Users must manually download definition files or rely on whatever happens to be bundled with their installation. This creates similar problems to what the Models API addressed: -1. **Version coupling**: Users are locked to whatever DFN version is bundled -2. **Manual management**: Users must manually track and download DFN updates -3. **No multi-version support**: Difficult to work with multiple MODFLOW 6 versions simultaneously -4. **Maintenance burden**: Developers must manually update bundled DFNs - -## Objective - -Create a DFNs API that: -1. **Mirrors Models/Programs API patterns** for consistency and familiarity -2. **Leverages existing dfn module work** (parsers, schemas, object models) -3. **Provides automated discovery** of definition files from MODFLOW 6 repository -4. **Supports multiple versions** simultaneously with explicit version addressing -5. **Uses Pooch** for fetching and caching (avoiding custom HTTP client code) -6. **Handles schema evolution** with proper separation of file format vs schema version -7. **Maintains loose coupling** between devtools and remote DFN sources - -## Overview - -Make the MODFLOW 6 repository responsible for publishing a definition file registry. - -Make `modflow-devtools` responsible for: -- Defining the DFN registry publication contract -- Providing registry-creation machinery -- Storing bootstrap information locating the MODFLOW 6 repository -- Discovering remote registries at install time or on demand -- Caching registry metadata and definition files -- Exposing a synchronized view of available definition files -- Parsing and validating definition files -- Mapping between schema versions - -MODFLOW 6 is currently the only repository using the DFN specification system, but this leaves the door open for other repositories to begin using it. - -## Architecture - -The DFNs API will mirror the Models and Programs API architecture, adapted for definition file-specific concerns. - -**Implementation approach**: Core classes are split across `modflow_devtools/dfns/__init__.py` (spec/parsing) and `modflow_devtools/dfns/registry.py` (registry infrastructure): -- `get_cache_dir()`: Cache directory path utility -- `BootstrapConfig` / `SourceConfig`: Pydantic models for bootstrap configuration -- `DfnRegistry`: Pydantic base class for registry access -- `RemoteDfnRegistry`: Remote fetching with Pooch integration -- `LocalDfnRegistry`: Local filesystem registry for development use -- `DfnRegistryMeta`: Pydantic model for `dfns.toml` registry file contents -- `DfnSpec`: Full specification with hierarchical and flat access -- `Dfn`, `Block`, `Field`: Core component dataclasses - -### Bootstrap file - -The **bootstrap** file tells `modflow-devtools` where to look for DFN registries. This file will be checked into the repository at `modflow_devtools/dfns/dfns.toml` and distributed with the package. - -#### Bootstrap file contents - -At the top level, the bootstrap file consists of a table of `sources`, each describing a repository that publishes definition files. - -Each source has: -- `repo`: Repository identifier (owner/name) -- `dfn_path`: Path within the repository to the directory containing DFN files (defaults to `doc/mf6io/mf6ivar/dfn`) -- `registry_path`: Path within the repository to the registry metadata file (defaults to `.registry/dfns.toml`) -- `refs`: List of git refs (branches, tags, or commit hashes) to sync by default - -#### User config overlay - -Users can customize or extend the bundled bootstrap configuration by creating a user config file at: -- Linux/macOS: `~/.config/modflow-devtools/dfns.toml` (respects `$XDG_CONFIG_HOME`) -- Windows: `%APPDATA%/modflow-devtools/dfns.toml` - -The user config follows the same format as the bundled bootstrap file. Sources defined in the user config will override or extend those in the bundled config, allowing users to: -- Add custom DFN repositories -- Point to forks of existing repositories (useful for testing experimental schema versions) -- Override default refs for existing sources - -**Implementation note**: The user config path logic (`get_user_config_path("dfn")`) is shared across all three APIs (Models, Programs, DFNs) via `modflow_devtools.config`, but each API implements its own `merge_bootstrap()` function using API-specific bootstrap schemas. - -#### Sample bootstrap file - -```toml -[sources.modflow6] -repo = "MODFLOW-ORG/modflow6" -dfn_path = "doc/mf6io/mf6ivar/dfn" -registry_path = ".registry/dfns.toml" -refs = [ - "6.6.0", - "6.5.0", - "6.4.4", - "develop", -] -``` - -### DFN spec and registry files - -The registry file (`dfns.toml`) is the metadata file that supports the DFNs API for discovery and distribution. - -#### Registry file format - -A **`dfns.toml`** registry file for **discovery and distribution** (the specific naming distinguishes it from `models.toml` and `programs.toml`): - -```toml -# Registry metadata (top-level, optional) -schema_version = "1.0" -generated_at = "2025-01-02T10:30:00Z" -devtools_version = "1.9.0" - -[metadata] -ref = "6.6.0" # Optional, known from discovery context - -# File listings (filenames and hashes, URLs constructed as needed) -[files] -"sim-nam.dfn" = {hash = "sha256:..."} -"sim-tdis.dfn" = {hash = "sha256:..."} -"gwf-nam.dfn" = {hash = "sha256:..."} -"gwf-chd.dfn" = {hash = "sha256:..."} -# ... all DFN files -``` - -**Notes**: -- Registry is purely **infrastructure** for discovery and distribution -- The `files` section maps filenames to hashes for verification -- URLs are constructed dynamically from bootstrap metadata (repo, ref, dfn_path) + filename -- This allows using personal forks by changing the bootstrap file -- **All registry metadata is optional** - registries can be handwritten minimally - -**Minimal handwritten registry**: -```toml -[files] -"sim-nam.dfn" = {hash = "sha256:def456..."} -"gwf-nam.dfn" = {hash = "sha256:789abc..."} -``` - -#### Sample files - -**Per-component TOML files** (current format in the MODFLOW 6 repository): - -Each component has its own `.toml` file named by component, e.g. `gwf-chd.toml`: - -```toml -name = "gwf-chd" -advanced = false -multi = true - -[options.auxiliary] -block = "options" -name = "auxiliary" -type = "string" -shape = "(naux)" -optional = true -description = "..." -# ... -``` +This document describes the design and architecture of the `modflow_devtools.dfns` module. It is intended for developers working on or extending `modflow-devtools`. -The registry lists all component files: -```toml -[files] -"sim-nam.toml" = {hash = "sha256:..."} -"gwf-nam.toml" = {hash = "sha256:..."} -"gwf-chd.toml" = {hash = "sha256:..."} -# ... all component files -``` - -**Single-blob TOML** (output of `DfnSpec.dump()`, used for `mf6 --spec`): - -`DfnSpec.dump()` serializes the entire spec as a single TOML document with each component as a top-level key: - -```toml -schema_version = "2" - -["gwf-chd"] -name = "gwf-chd" -advanced = false -multi = true - -["gwf-chd".options.auxiliary] -block = "options" -name = "auxiliary" -# ... - -["gwf-dis"] -name = "gwf-dis" -# ... -``` - -This format requires no preprocessing — consumers can pipe `mf6 --spec` output directly into `tomllib`. Hierarchy is preserved via `parent` attributes embedded in each component's data, and can be reconstructed by `DfnSpec.load()` using naming convention inference (`to_tree()`). - -### Registry discovery - -DFN registries can be discovered in two modes, similar to the Models API. - -#### Discovery modes - -**1. Registry as version-controlled file**: - -Registry files can be versioned in the repository at a conventional path, in which case discovery uses GitHub raw content URLs: - -``` -https://raw.githubusercontent.com/{org}/{repo}/{ref}/.registry/dfns.toml -``` - -This mode supports any git ref (branches, tags, commit hashes). - -**2. Registry as release asset**: - -Registry files can also be published as release assets: - -``` -https://github.com/{org}/{repo}/releases/download/{tag}/dfns.toml -``` - -This mode: -- Requires release tags only -- Allows registry generation in CI without committing to repo -- Provides faster discovery (no need to check multiple ref types) - -**Discovery precedence**: Release asset mode takes precedence if both exist (same as Models API). - -#### Registry discovery procedure - -At sync time, `modflow-devtools` discovers remote registries for each configured ref: +## Background -1. **Check for release tag** (if release asset mode enabled): - - Look for a GitHub release with the specified tag - - Try to fetch `dfns.toml` from release assets - - If found, use it and skip step 2 - - If release exists but lacks registry asset, fall through to step 2 +MODFLOW 6 describes its input format in *definition files* (DFNs). The `modflow_devtools.dfns` module provides a structured, typed Python API for loading and navigating those definitions. -2. **Check for version-controlled registry**: - - Look for a commit hash, tag, or branch matching the ref - - Try to fetch registry from `{registry_path}` via raw content URL - - If found, use it - - If ref exists but lacks registry file, raise error: - ```python - DfnRegistryDiscoveryError( - f"Registry file not found in {registry_path} for 'modflow6@{ref}'" - ) - ``` +The module complements the older `modflow_devtools.dfn` module, which provides simpler utilities for parsing the legacy flat text format. `modflow_devtools.dfn` remains stable; `modflow_devtools.dfns` is experimental and may change. -3. **Failure case**: - - If no matching ref found at all, raise error: - ```python - DfnRegistryDiscoveryError( - f"Registry discovery failed, ref 'modflow6@{ref}' does not exist" - ) - ``` +## Architecture overview -**Note**: For initial implementation, focus on version-controlled mode. Release asset mode requires MODFLOW 6 to start distributing DFN files with releases (currently they don't), but would be a natural addition once that happens. +The module is split across three files: -### Registry/DFN caching +| File | Responsibility | +|---|---| +| `schema.py` | Pydantic models for all field types, blocks, and components; the `Dfns` top-level class | +| `registry.py` | Registry classes for local and remote DFN sources | +| `mapper.py` | Maps v1 `.dfn`-format data to the v2 schema used by `schema.py` | +| `__main__.py` | CLI entry point (`sync`, `info`, `clean`) | +| `dfns.toml` | Bundled list of default remote release IDs | -Cache structure mirrors the Models API pattern: +## Schema (`schema.py`) -``` -~/.cache/modflow-devtools/ -├── dfn/ -│ ├── registries/ -│ │ └── modflow6/ # by source repo -│ │ ├── 6.6.0/ -│ │ │ └── dfns.toml -│ │ ├── 6.5.0/ -│ │ │ └── dfns.toml -│ │ └── develop/ -│ │ └── dfns.toml -│ └── files/ # Actual DFN files, managed by Pooch -│ └── modflow6/ -│ ├── 6.6.0/ -│ │ ├── sim-nam.dfn -│ │ ├── gwf-nam.dfn -│ │ └── ... -│ ├── 6.5.0/ -│ │ └── ... -│ └── develop/ -│ └── ... -``` +### Field types -**Cache management**: -- Registry files cached per source repository and ref -- DFN files fetched and cached individually by Pooch, verified against registry hashes -- Cache persists across Python sessions for offline use -- Cache can be cleared with `dfn clean` command -- Users can check cache status with `dfn info` +Fields are Pydantic models, all inheriting from `FieldBase`. Scalar types are `Keyword`, `String`, `Integer`, `Double`, and `File`. Composite types are `Array`, `Record`, `Union`, and `List`. Each has a frozen `type` literal discriminator field, which drives Pydantic's discriminated-union validation. -### Registry synchronization +`FieldBase.from_dict(d, strict=False)` is the low-level factory: it reads the `type` key and dispatches to the appropriate subclass. -Synchronization updates the local registry cache with remote metadata. +### Blocks -#### Manual sync +`Block` is a Pydantic model with `name`, `fields` (ordered dict), and `repeats`. Its `optional` property is derived: a block is optional iff all its fields are optional. -Exposed as a CLI command and Python API: +`Blocks` is a type alias for `Mapping[str, Block]`. -```bash -# Sync all configured refs -python -m modflow_devtools.dfns sync +### Components -# Sync specific ref -python -m modflow_devtools.dfns sync --ref 6.6.0 +Three component types are distinguished by a `type` discriminator: -# Sync to any git ref (branch, tag, commit hash) -python -m modflow_devtools.dfns sync --ref develop -python -m modflow_devtools.dfns sync --ref f3df630a +- `Simulation` — always the root; `parent` is `null` +- `Model` — adds a `solution` field (compatible solution type) +- `Package` — adds `multi` (bool) and `subtype` -# Force re-download -python -m modflow_devtools.dfns sync --force +`Component` is an annotated discriminated union of the three. -# Show sync status -python -m modflow_devtools.dfns info +`ComponentBase` is the shared Pydantic base class. It carries `name`, `blocks`, `parent`, `schema_version`, and `derived_dims`. -# List available DFNs for a ref -python -m modflow_devtools.dfns list --ref 6.6.0 +### `Dfns` -# List all synced refs -python -m modflow_devtools.dfns list -``` +`Dfns` is a Pydantic model that holds a `components` dict. It is the top-level object produced by loading a directory of DFN files. -Or via Python API: +Key members: -```python -from modflow_devtools.dfns import sync_dfns, get_sync_status +| Member | Type | Description | +|---|---|---| +| `components` | `dict[str, Component]` | All components, keyed by name | +| `schema_version` | `str` (computed) | Version string from components, or `"2"` | +| `root` | `Simulation \| None` | The simulation component, or `None` | +| `children_of(name)` | `dict[str, Component]` | All components whose `parent == name` | +| `explicit_dims_for(name)` | `set[str]` | Explicit dimension names for a component | +| `grid_dims_for(name)` | `set[str]` | Dims inherited from other components | +| `load(path)` | classmethod | Load a directory of `.dfn` or `.toml` files | -# Sync all configured refs -sync_dfns() +`Dfns.load()` supports both formats in the same directory. `.dfn` files are parsed by the `modflow_devtools.dfn.schema` module and then mapped to v2 schema objects via `mapper.map`. TOML files carry v2 content directly and are loaded with `tomli` and passed straight to the Pydantic validator. -# Sync specific ref -sync_dfns(ref="6.6.0") +Two model validators run at construction time: +- `_validate_schema_version_consistency` — all non-null `schema_version` values must agree. +- `_validate_dims_and_shapes` — validates `derived_dims` expressions and all `Array.shape` elements. -# Check sync status -status = get_sync_status() -``` +### Array dimension validation -#### Automatic sync +The validation logic in `schema.py` is non-trivial. Three resolution scopes are checked for each shape element: -- **At install time**: Best-effort sync to default refs during package installation (fail silently on network errors) -- **On first use**: If registry cache is empty for requested ref, attempt to sync before raising errors -- **Lazy loading**: Don't sync until DFN access is actually requested -- **Configurable (Experimental)**: Auto-sync is opt-in via environment variable: `MODFLOW_DEVTOOLS_AUTO_SYNC=1` (set to "1", "true", or "yes") +1. **Local explicit dims** — `Integer` or `Array` fields with a `dimension` attribute. +2. **Local derived dims** — entries in `component.derived_dims`. +3. **Grid dims** — explicit dims from other components in the spec, filtered by scope. +4. **Intra-record sibling** — fallback for array subfields of records; resolves to a sibling `Integer` with `dimension="record"`. -### Source repository integration +Row-level column lookup expressions (`block.column(fk_field)`) are also validated structurally. -For the MODFLOW 6 repository to integrate: +`derived_dims` expressions are validated for well-formedness (Python arithmetic syntax), operand scope, and absence of cycles (topological sort). -1. **Generate registry** in CI: - ```bash - # In MODFLOW 6 repository CI - python -m modflow_devtools.dfns.make_registry \ - --dfn-path doc/mf6io/mf6ivar/dfn \ - --output .registry/dfns.toml \ - --ref ${{ github.ref_name }} - ``` +## Mapper (`mapper.py`) -2. **Commit registry** to `.registry/dfns.toml` +The mapper converts a v1 `Dfn` object (from `modflow_devtools.dfn.schema`) to a v2 `Component`. The entry point is `map(dfn: v1.Dfn) -> Component`. It raises `ValueError` if the input schema version is not `"1"` or `"1.1"`. -3. **Example CI integration** (GitHub Actions): - ```yaml - - name: Generate DFN registry - run: | - pip install modflow-devtools - python -m modflow_devtools.dfns.make_registry \ - --dfn-path doc/mf6io/mf6ivar/dfn \ - --output .registry/dfns.toml \ - --ref ${{ github.ref_name }} +Component type is inferred from the component name: +- `sim-nam` → `Simulation` +- `*-nam` → `Model` +- `sln-*` → `Package(subtype="solution")` +- `exg-*` → `Package(subtype="exchange")` +- `utl-*` → `Package(subtype="utility")` +- advanced flag set → `Package(subtype="advanced")` +- all others → `Package` - - name: Commit registry - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add .registry/dfns.toml - git diff-index --quiet HEAD || git commit -m "chore: update DFN registry" - git push - ``` +v1 field types are mapped to v2 equivalents. `recarray` fields become `List` with a nested `Record` item. `record` fields become `Record`. `in_record` fields are promoted into their enclosing record's `fields` dict. -**Note**: Initially generate registries for version-controlled mode. Release asset mode would require MODFLOW 6 to start distributing DFNs with releases. +## Registry (`registry.py`) -### DFN addressing +### `DfnRegistry` -**Format**: `mf6@{ref}/{component}` +Pydantic base class. Declares a `_spec` private attribute and stubs `spec` and `get_path()` for subclasses. -Components include: -- `ref`: Git ref (branch, tag, or commit hash) corresponding to a MODFLOW 6 version -- `component`: DFN component name (without file extension) +### `LocalDfnRegistry` -Examples: -- `mf6@6.6.0/sim-nam` - Simulation name file definition for MODFLOW 6 v6.6.0 -- `mf6@6.6.0/gwf-chd` - GWF CHD package definition for v6.6.0 -- `mf6@develop/gwf-wel` - GWF WEL package definition from develop branch -- `mf6@f3df630a/gwt-adv` - GWT ADV package definition from specific commit +Takes a `path` field. On first access to `.spec`, calls `Dfns.load(self.path)` and caches the result. `get_path(component)` searches for `.dfn` then `.toml` in the directory. -**Benefits**: -- Explicit versioning prevents confusion -- Supports multiple MODFLOW 6 versions simultaneously -- Enables comparison between versions -- Works with any git ref (not just releases) +### `RemoteDfnRegistry` -**Note**: The source is always "mf6" (MODFLOW 6), but the addressing scheme allows for future sources if needed. +Takes a `release_id` string of the form `"owner/repo@tag"`, where `tag` may be a specific version string or `"latest"`. -### Registry classes +**Tag resolution**: `latest_tag()` returns the tag part directly if it's not `"latest"`. For `"latest"`, it queries the GitHub API (`/releases/latest`) once and caches the result in `_latest`. -The registry class hierarchy is based on a Pydantic `DfnRegistry` base class (in `modflow_devtools/dfns/registry.py`): +**Cache path**: `~/.cache/modflow-devtools/dfns/{owner}/{repo}/{resolved_tag}/` on Unix; `%LOCALAPPDATA%/...` on Windows. Respects `XDG_CACHE_HOME`. -**`DfnRegistry` (base class)**: -- Pydantic model with `source` and `ref` fields -- Abstract `spec` property and `get_dfn_path()` method for subclasses to implement -- Concrete helpers: - - `get_dfn(component)` - convenience for `spec[component]` - - `schema_version` - convenience for `spec.schema_version` - - `components` - convenience for `dict(spec.items())` +**Sync**: `sync(force=False)` downloads `dfns.zip` from `https://github.com/{repo}/releases/download/{tag}/dfns.zip`, extracts it into `cache_path` using `pooch`. Skips if the cache dir already contains files and `force=False`. -**`RemoteDfnRegistry(DfnRegistry)`**: +**Spec access**: `.spec` calls `sync()` if the cache is empty, then calls `Dfns.load(cache_path)`. -Handles remote registry discovery, caching, and DFN fetching. Constructs DFN file URLs dynamically from `BootstrapConfig`/`SourceConfig` — URLs are never stored in the registry file itself. +**`load(path)`** classmethod: reads a TOML file with a `releases` list of release ID strings and returns a dict of `RemoteDfnRegistry` objects. -Optional field overrides (`repo`, `dfn_path`, `registry_path`) allow bypassing the bootstrap config, e.g. for testing against a personal fork: +**`load_default()`** classmethod: loads the bundled `dfns.toml` config, then merges any user config overlay (see below). Returns a merged dict, with user config entries taking precedence. -```python -# Use bootstrap config (normal usage) -registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") +**`from_ids(*ids)`** classmethod: creates registries from ID strings; auto-syncs if `MODFLOW_DEVTOOLS_AUTO_SYNC` is set and the cache is empty. -# Override repo directly (e.g., testing a fork) -registry = RemoteDfnRegistry( - source="modflow6", - ref="registry", - repo="wpbonelli/modflow6", -) -``` +**`cached_tag()`**: returns the cached tag without network access. For exact tags, checks whether `cache_path` exists and is non-empty. For `@latest`, scans the repo's cache directory and returns the most-recently-modified tag directory's name. -Key methods: `sync(force=False)`, `get_dfn_path(component)`, `registry_meta` property. +### Bootstrap and user config -**`LocalDfnRegistry(DfnRegistry)`**: +The bundled config is at `modflow_devtools/dfns/dfns.toml`: -For developers working with a local DFN directory: - -```python -registry = LocalDfnRegistry(path="/path/to/mf6/doc/mf6io/mf6ivar/dfn") -dfn = registry.get_dfn("gwf-chd") -``` - -Loads `DfnSpec` lazily via `DfnSpec.load(path)` on first access. - -**Supporting Pydantic models** (in `registry.py`): -- `BootstrapConfig` / `SourceConfig`: bootstrap file schema (sources, refs, paths) -- `DfnRegistryMeta`: `dfns.toml` registry file schema (schema_version, generated_at, files) -- `DfnRegistryFile`: per-file entry with SHA256 hash - -**Design decisions**: -- **Pydantic-based** (not ABC) — allows Pydantic validation and field introspection -- **Dynamic URL construction** — DFN file URLs constructed at runtime from bootstrap metadata, not stored in registry files -- **No `MergedRegistry`** — users work with one MODFLOW 6 version at a time - -### Module-level API - -Convenient module-level functions: - -```python -from modflow_devtools.dfns import ( - DfnSpec, - get_dfn, - get_dfn_path, - list_components, - sync_dfns, - get_registry, - map, -) - -# Get individual DFNs (defaults to ref="develop") -dfn = get_dfn("gwf-chd") -dfn = get_dfn("gwf-chd", ref="6.5.0") # Specific version - -# Get file path -path = get_dfn_path("gwf-wel", ref="6.6.0") - -# List available components -components = list_components(ref="6.6.0") - -# Work with specific registry -registry = get_registry(ref="6.6.0") -gwf_nam = registry.get_dfn("gwf-nam") - -# Load full specification - single canonical hierarchical representation -spec = DfnSpec.load("/path/to/dfns") # Load from directory - -# Hierarchical access -spec.schema_version # Version('2') when loaded from legacy .dfn files (auto-mapped) -spec.root # Root Dfn (simulation component) -spec.root.children["gwf-nam"] # Navigate hierarchy -spec.root.children["gwf-nam"].children["gwf-chd"] - -# Flat dict-like access via Mapping protocol -gwf_chd = spec["gwf-chd"] # Get component by name -for name, dfn in spec.items(): # Iterate all components - print(name) -len(spec) # Total number of components - -# Access spec through registry (registry provides the spec) -registry = get_registry(ref="6.6.0") -spec = registry.spec # Registry wraps a DfnSpec -gwf_chd = registry.spec["gwf-chd"] - -# Map between schema versions -dfn_v1 = get_dfn("gwf-chd", ref="6.4.4") # Older version in v1 schema -dfn_v2 = map(dfn_v1, schema_version="2") # Convert to v2 schema +```toml +releases = [ + "MODFLOW-ORG/modflow6@latest", + "MODFLOW-ORG/modflow6-nightly-build@latest", +] ``` -**`DfnSpec` class**: - -The `DfnSpec` dataclass represents the full specification with a single canonical hierarchical representation: +The user config path (`RemoteDfnRegistry.user_config_path()`) is: +- Linux/macOS: `$XDG_CONFIG_HOME/modflow-devtools/dfns.toml` (default `~/.config/`) +- Windows: `%APPDATA%/modflow-devtools/dfns.toml` -```python -from collections.abc import Mapping -from dataclasses import dataclass +Both files use the same format. `load_default()` merges them with `base | user` (user entries override base entries of the same key). -@dataclass -class DfnSpec(Mapping): - """Full DFN specification with hierarchical structure and flat dict access.""" +### Auto-sync - schema_version: str - root: Dfn # Hierarchical canonical representation (simulation component) +`_auto_sync()` checks whether `MODFLOW_DEVTOOLS_AUTO_SYNC` is set to a truthy value (`"1"`, `"true"`, or `"yes"`). When true, `from_ids()` calls `sync()` for any registry whose cache is empty. - # Mapping protocol - provides flat dict-like access - def __getitem__(self, name: str) -> Dfn: - """Get component by name (flattened lookup).""" - ... +## CLI (`__main__.py`) - def __iter__(self): - """Iterate over all component names.""" - ... +Three subcommands: - def __len__(self): - """Total number of components in the spec.""" - ... +| Command | Action | +|---|---| +| `sync [--force/-f]` | Call `sync()` on each registry from `load_default()` | +| `info` | Call `cached_tag()` on each registry and print cache status | +| `clean` | Delete the entire base cache directory | - @classmethod - def load(cls, path: Path | str) -> "DfnSpec": - """ - Load specification from a directory of DFN files. +The CLI entry point is `main(argv=None)`. - The specification is always loaded as a hierarchical tree, - with flat access available via the Mapping protocol. - """ - ... -``` +## Relationship to `modflow_devtools.dfn` -**Design benefits**: -- **Single canonical representation**: Hierarchical tree is the source of truth -- **Flat access when needed**: Mapping protocol provides dict-like interface -- **Simple, focused responsibility**: `DfnSpec` only knows how to load from a directory -- **Clean layering**: Registries built on top of `DfnSpec`, not intertwined -- **Clean semantics**: `DfnSpec` = full specification, `Dfn` = individual component -- **Pythonic**: Implements standard `Mapping` protocol - -**Separation of concerns**: -- **`DfnSpec`**: Canonical representation of the full specification (foundation) - - Loads from a directory of DFN files via `load()` classmethod - - Hierarchical tree via `.root` property - - Flat dict access via `Mapping` protocol - - No knowledge of registries, caching, or remote sources -- **Registries**: Handle discovery, distribution, and caching (built on DfnSpec) - - Fetch and cache DFN files from remote sources - - Internally use `DfnSpec` to represent the loaded specification - - Provide access via `.spec` property - - `get_dfn(component)` → convenience for `spec[component]` - - `get_dfn_path(component)` → returns cached file path - -Backwards compatibility with existing `fetch_dfns()`: - -```python -# Old API — still works for manual downloads (stable modflow_devtools.dfn module) -from modflow_devtools.dfn import get_dfns -get_dfns("MODFLOW-ORG", "modflow6", "6.6.0", "/tmp/dfns") - -# New API (preferred - uses registry and caching) -from modflow_devtools.dfns import sync_dfns, get_registry, DfnSpec -sync_dfns(ref="6.6.0") -registry = get_registry(ref="6.6.0") -spec = registry.spec # Registry wraps a DfnSpec -``` +The v1 `modflow_devtools.dfn` module remains the stable baseline. `modflow_devtools.dfns` builds on top of it: `Dfns.load()` imports `modflow_devtools.dfn.schema` to parse `.dfn` files, and `mapper.py` converts the resulting v1 objects to v2 schema. The v1 module's `fetch_dfns()` function is re-exported from `modflow_devtools.dfns.__init__` for convenience. -## Schema Versioning - -A key design consideration is properly handling schema evolution while separating file format from schema version. - -### Separating format from schema - -As discussed in [issue #259](https://github.com/MODFLOW-ORG/modflow-devtools/issues/259), **file format and schema version are orthogonal concerns**: - -**File format** (serialization): -- `dfn` - Legacy DFN text format -- `toml` - Modern TOML format (or potentially YAML, see below) - -The format is simply how the data is serialized to disk. Any schema version can be serialized in any supported format. - -**Schema version** (structural specification): -- Defines what components exist and how they relate to each other -- Defines which variables each component contains -- Defines variable types, shapes, and constraints -- Separates structural specification from input format representation concerns - -The schema describes the semantic structure and meaning of the specification, independent of how it's serialized. - -**Key distinction**: The schema migration is about separating structural specification (components, relationships, variables, types) from input format representation. This is discussed in detail in [pyphoenix-project issue #246](https://github.com/modflowpy/pyphoenix-project/issues/246). - -For example: -- **Input format issue** (v1): Period data defined as recarrays with artificial dimensions like `maxbound` -- **Structural reality** (v2): Each column is actually a variable living on (a subset of) the grid, using semantically meaningful dimensions - -The v1 schema conflates: -- **Structural information**: Components, their relationships, and variables within each component -- **Format information**: How MF6 allows arrays to be provided, when keywords like `FILEIN`/`FILEOUT` are necessary - -The v2 schema should treat these as **separate layers**, where consumers can selectively apply formatting details atop a canonical data model. - -**Current state**: -- The code supports loading both `dfn` and `toml` formats -- The `Dfn.load()` function accepts a `format` parameter -- Schema version is determined independently of file format -- V1→V1.1 and V1→V2 schema mapping is implemented - -**Implications for DFNs API**: -- Registry metadata includes both `format` and `schema_version` fields -- Registries can have different formats at different refs (some refs: dfn, others: toml) -- The same schema version can be serialized in different formats -- Schema mapping happens after loading, independent of file format -- Users can request specific schema versions via `map()` function - -### Schema evolution - -**v1 schema** (original): -- Current MODFLOW 6 releases through 6.6.x -- Flat structure with `in_record`, `tagged`, `preserve_case`, etc. attributes -- Mixes structural specification with input format representation (recarray/maxbound issue) -- Can be serialized as `.dfn` (original) or `.toml` - -**v1.1 schema** (intermediate): -- Cleaned-up v1 with data normalization -- Removed unnecessary attributes (`in_record`, `tagged`, etc.) -- Structural improvements (period block arrays separated into individual variables) -- Better parent-child relationships inferred from naming conventions -- Can be serialized as `.dfn` or `.toml` -- **Recommendation from issue #259**: Use this as the mainline, not jump to v2 - -**v2 schema** (future - comprehensive redesign): -- For devtools 2.x / FloPy 4.x / eventually MF6 -- **Explicit parent-child relationships** via `parent` attributes in per-component TOML files (no inference needed) -- **Complete separation of structural specification from input format concerns** (see [pyphoenix-project #246](https://github.com/modflowpy/pyphoenix-project/issues/246)) - - Structural layer: components, relationships, variables, data models - - Format layer: how MF6 allows arrays to be provided, FILEIN/FILEOUT keywords, etc. - - Consumers can selectively apply formatting details atop canonical data model -- **Explicit parent-child relationships in DFN files** (see Component Hierarchy section) -- Modern type system with proper array types and semantically meaningful dimensions -- Consolidated attribute representation (see Tentative v2 schema design) -- Likely serialized as TOML or YAML (with JSON-Schema validation via Pydantic) - -**DFNs API strategy**: -- Support all schema versions via registry metadata -- Provide transparent schema mapping where needed -- Default to native schema version from registry -- Allow explicit schema version selection via API -- Maintain backwards compatibility during transitions - -## Schema version support - -The DFNs API will support **multiple schema versions simultaneously**: - -```python -# Schema version is tracked per registry/ref -registry_v1 = get_registry(ref="6.4.4") # MODFLOW 6.4.4 uses v1 schema -registry_v11 = get_registry(ref="6.6.0") # MODFLOW 6.6.0 uses v1.1 schema -registry_v2 = get_registry(ref="develop") # Future: develop uses v2 schema - -# Get DFN in native schema version -dfn_v1 = registry_v1.get_dfn("gwf-chd") # Returns v1 schema -dfn_v11 = registry_v11.get_dfn("gwf-chd") # Returns v1.1 schema - -# Transparently map to desired schema version -from modflow_devtools.dfns import map -dfn_v2 = map(dfn_v1, schema_version="2") # v1 → v2 -dfn_v2 = map(dfn_v11, schema_version="2") # v1.1 → v2 -``` +## Testing -**Registry support**: -- Each registry metadata includes `schema_version` (from component files or inferred) -- Different refs can have different schema versions -- `RemoteDfnRegistry` loads appropriate schema version for each ref -- `load()` function detects schema version and uses appropriate parser/validator - -**Schema detection**: -```python -# In RemoteDfnRegistry or DfnSpec.load() -def _detect_schema_version(self) -> Version: - # 1. Infer from component file content (schema_version field) - sample_dfn = self._load_sample_dfn() - return infer_schema_version(sample_dfn) - - # 2. Default to latest stable - return Version("1.1") -``` +Tests for the dfns module live under `autotest/dfns/`: +- `test_dfns.py` — tests for `Dfns.load()`, `children_of()`, and the CLI +- `test_dfns_registry.py` — tests for `LocalDfnRegistry`, `RemoteDfnRegistry`, and caching behavior +- `test_dfns_schema.py` — tests for schema validation (dims, shapes, fk/pk) +- `test_mapper.py` — unit tests for the v1→v2 mapper -## Implementation Dependencies - -### Completed work - -The `modflow_devtools.dfns` package is implemented in full. The following is a summary of what exists: - -- ✅ `Dfn`, `Block`, `Field` dataclasses (in `__init__.py`) -- ✅ Schema definitions (`FieldV1`, `FieldV2`) (in `schema/`) -- ✅ Parsers for both DFN and TOML formats (`parse.py`, `load()`, `load_flat()`, `load_tree()`) -- ✅ Schema mapping (V1 → V2) with `MapV1To2` -- ✅ Hierarchy inference via `to_tree()` / `to_flat()` -- ✅ `DfnSpec` dataclass with `Mapping` protocol and `load()` classmethod -- ✅ `DfnSpec.dump()` / `DfnSpec.dumps()` — serialize full spec as single TOML blob -- ✅ Validation utilities (`is_valid()`) -- ✅ `dfn2toml` conversion tool (`dfn2toml.py`) -- ✅ Bootstrap file and registry schema (`BootstrapConfig`, `SourceConfig`, `DfnRegistryMeta`) -- ✅ Registry classes (`DfnRegistry`, `RemoteDfnRegistry`, `LocalDfnRegistry`) (in `registry.py`) -- ✅ Registry discovery and synchronization (`sync_dfns()`, `get_sync_status()`) -- ✅ Pooch integration for file caching -- ✅ Module-level convenience API (`get_dfn`, `get_dfn_path`, `list_components`, `get_registry`) -- ✅ CLI (`__main__.py`): `sync`, `info`, `list`, `clean` -- ✅ Registry generation tool (`make_registry.py`) -- ⚠️ Integration with MODFLOW 6 CI (requires registry branch merge in MF6 repo) - -The legacy `modflow_devtools.dfn` module (`dfn.py`) remains alongside the new package for backwards compatibility. - -**Implementation status** (DFNs API): -- ✅ Bootstrap file and registry schema (`BootstrapConfig`, `SourceConfig`, `DfnRegistryMeta`) -- ✅ Registry discovery and synchronization -- ✅ Pooch integration for file caching -- ✅ Registry classes (`DfnRegistry`, `RemoteDfnRegistry`, `LocalDfnRegistry`) -- ✅ CLI commands (sync, info, list, clean) -- ✅ Module-level convenience API (`get_dfn`, `get_dfn_path`, `list_components`, `sync_dfns`, `get_registry`) -- ✅ Registry generation tool (`make_registry.py`) -- ✅ `DfnSpec.dump()` / `DfnSpec.dumps()` — serialize full spec as single TOML blob -- ⚠️ Integration with MODFLOW 6 CI (requires registry branch merge in MF6 repo) - -### Core components - -**Foundation** (no dependencies): -1. ✅ Core dfns package (schema, parser, utility code) — already merged -2. Add bootstrap file (`modflow_devtools/dfns/dfns.toml`) -3. Define registry schema with Pydantic (handles validation and provides JSON-Schema export) -4. Implement registry discovery logic -5. Create cache directory structure utilities - -**Registry infrastructure** (depends on Foundation): -1. Add Pooch as dependency -2. Implement `DfnRegistry` abstract base class -3. Implement `RemoteDfnRegistry` with Pooch for file fetching -4. Refactor existing code into `LocalDfnRegistry` -5. Implement `sync_dfns()` function -6. Add registry metadata caching with hash verification -7. Implement version-controlled registry discovery -8. Add auto-sync on first use (opt-in via `MODFLOW_DEVTOOLS_AUTO_SYNC` while experimental) -9. **Implement `DfnSpec` dataclass** with `Mapping` protocol for single canonical hierarchical representation with flat dict access - -**CLI and module API** (depends on Registry infrastructure): -1. Create `modflow_devtools/dfns/__main__.py` -2. Add commands: `sync`, `info`, `list`, `clean` -3. Add `--ref` flag for version selection -4. Add `--force` flag for re-download -5. Add convenience functions (`get_dfn`, `get_dfn_path`, `list_components`, etc.) -6. Default `ref="develop"` in `get_registry()` / `get_dfn()` etc. for "latest" access -7. Maintain backwards compatibility with `fetch_dfns()` - -**Registry generation tool** (depends on Foundation): -1. Implement `modflow_devtools/dfns/make_registry.py` -2. Scan DFN directory and generate **registry file** (`dfns.toml`): file listings with hashes -3. Compute file hashes (SHA256) for all DFN/TOML files -4. Registry output: just filename -> hash mapping (no URLs - constructed dynamically) -5. Support both full output (for CI) and minimal output (for handwriting) -6. For v1/v1.1: infer hierarchy from naming conventions for validation -7. For v2: read explicit `parent` attributes from component files for validation - -### MODFLOW 6 repository integration - -**CI workflow** (depends on Registry generation tool): -1. Install modflow-devtools in MODFLOW 6 CI -2. Generate registry on push to develop and release tags -3. Commit registry to `.registry/dfns.toml` -4. Test registry discovery and sync -5. **Note**: No separate `spec.toml` is needed — hierarchy is inferred from naming conventions for v1/v1.1, or read from `parent` attributes in component files for v2 - -**Bootstrap configuration** (depends on MODFLOW 6 CI): -1. Add stable MODFLOW 6 releases to bootstrap refs (6.6.0, 6.5.0, etc.) -2. Include `develop` branch for latest definitions -3. Test multi-ref discovery and sync - -### Testing and documentation - -**Testing** (depends on all core components): -1. Unit tests for registry classes -2. Integration tests for sync mechanism -3. Network failure scenarios -4. Multi-version scenarios -5. Schema mapping tests (v1 → v1.1 → v2) -6. Both file format tests (dfn and toml) -7. Backwards compatibility tests with existing FloPy usage - -**Documentation** (can be done concurrently with implementation): -1. Update `docs/md/dfn.md` with API examples -2. Document format vs schema separation clearly -3. Document schema evolution roadmap (v1 → v1.1 → v2) -4. Document component hierarchy approach (explicit in DFN files for v2) -5. Add migration guide for existing code -6. CLI usage examples -7. MODFLOW 6 CI integration guide - -## Relationship to Models and Programs APIs - -The DFNs API deliberately mirrors the Models and Programs API architecture for consistency: - -| Aspect | Models API | Programs API | **DFNs API** | -|--------|-----------|--------------|--------------| -| **Bootstrap file** | `models/models.toml` | `programs/programs.toml` | `dfns/dfns.toml` | -| **Registry format** | TOML with files/models/examples | TOML with programs/binaries | TOML with files/components/hierarchy | -| **Discovery** | Release assets or version control | Release assets only | Version control (+ release assets future) | -| **Caching** | `~/.cache/.../models` | `~/.cache/.../programs` | `~/.cache/.../dfn` | -| **Addressing** | `source@ref/path/to/model` | `program@version` | `mf6@ref/component` | -| **CLI** | `models sync/info/list` | `programs sync/info/install` | `dfns sync/info/list/clean` | -| **Primary use** | Access model input files | Install program binaries | Parse definition files | - -**Key differences**: -- DFNs API focuses on metadata/parsing, not installation -- DFNs API leverages existing parser infrastructure (Dfn, Block, Field classes) -- DFNs API handles schema versioning/mapping (format vs schema separation) -- DFNs API supports both flat and hierarchical representations - -**Shared patterns**: -- Bootstrap-driven discovery -- Remote sync with Pooch caching -- Ref-based versioning (branches, tags, commits) -- CLI command structure -- Lazy loading / auto-sync on first use -- Environment variable opt-out for auto-sync - -This consistency benefits both developers and users with a familiar experience across all three APIs. - -## Cross-API Consistency - -The DFNs API follows the same design patterns as the Models and Programs APIs for consistency. See the **Cross-API Consistency** section in `models.md` for full details. - -**Key shared patterns**: -- Pydantic-based registry classes (not ABCs) -- Dynamic URL construction (URLs built at runtime, not stored in registries) -- Bootstrap and user config files with identical naming (`dfns.toml`), distinguished by location -- Top-level `schema_version` metadata field -- Distinctly named registry file (`dfns.toml`) -- Shared config utility: `get_user_config_path("dfn")` - -**Unique to DFNs API**: -- Discovery via version control (release assets mode planned for future) -- Extra `dfn_path` bootstrap field (location of DFN files within repo) -- Schema versioning and mapping capabilities -- No `MergedRegistry` (users work with one MF6 version at a time) - -### Future enhancements - -1. **Release asset mode**: Add support for registries as release assets (in addition to version control) -2. **Registry compression**: Compress registry files for faster downloads -3. **Partial updates**: Diff-based registry synchronization -4. **Offline mode**: Explicit offline mode that never attempts sync -5. **Conda integration**: Coordinate with conda-forge for bundled DFN packages -6. **Multi-source support**: Support definition files from sources other than MODFLOW 6 -7. **Validation API**: Expose validation functionality for user-provided input files -8. **Diff/compare API**: Compare DFNs across versions to identify changes +Network-dependent tests (`test_latest_tag_live`, `test_remote_dfn_registry_sync`) are skipped by default with `@pytest.mark.skip`. The registry tests use `unittest.mock.patch` to avoid network calls. diff --git a/docs/md/dfns.md b/docs/md/dfns.md index 9a3ae5b0..9d266553 100644 --- a/docs/md/dfns.md +++ b/docs/md/dfns.md @@ -4,8 +4,8 @@ MODFLOW 6 specifies input components and their variables in configuration files `modflow_devtools` provides two modules for working with MODFLOW 6 input specification files: -- **`modflow_devtools.dfn`:** stable, soon-to-be deprecated -- **`modflow_devtools.dfns`:** experimental, subject to change without notice +- **`modflow_devtools.dfn`:** stable utilities for parsing legacy `.dfn` files +- **`modflow_devtools.dfns`:** experimental structured API, subject to change without notice ## `modflow_devtools.dfn` (stable) @@ -14,12 +14,12 @@ The stable `modflow_devtools.dfn` module provides basic utilities for parsing le ### Downloading definition files ```python -from modflow_devtools.dfn import get_dfns +from modflow_devtools.dfn import fetch_dfns -get_dfns("MODFLOW-ORG", "modflow6", "6.6.0", "/tmp/dfns") +fetch_dfns("MODFLOW-ORG", "modflow6", "6.6.0", "/tmp/dfns") ``` -Downloads all `.dfn` files for the specified MODFLOW 6 release into the given output directory (returns `None`). +Downloads all `.dfn` files for the specified MODFLOW 6 release into the given output directory. ### Converting to TOML @@ -41,202 +41,241 @@ The tool may also be used on individual files. To validate legacy format files, ## `modflow_devtools.dfns` (experimental) -> **Note**: This module is experimental. The API may change without following normal deprecation procedures. +> **Note**: This module is experimental. The API may change without following normal deprecation procedures. To suppress the warning emitted on import, use: +> ```python +> import warnings +> warnings.filterwarnings('ignore', message='.*modflow_devtools.dfns.*experimental.*') +> ``` -The `modflow_devtools.dfns` module provides a richer API for working with MODFLOW 6 input specifications, including structured Python objects, a registry system for remote discovery and caching, and serialization to TOML. +The `modflow_devtools.dfns` module provides a structured API for working with MODFLOW 6 input specifications, including typed Python objects representing each component and field type, a registry system for remote caching, and tools for loading and navigating the full specification. -### Formats +### File format and schema version -MODFLOW 6 input specifications exist in two formats: +These are two separate concerns. -**Legacy DFN format** (`.dfn` files): The original text-based format, used in current MODFLOW 6 releases. Flat lists of variables with comments demarcating blocks. +**File format** is the serialization: -**TOML format** (`.toml` files): A structured, hierarchical representation. Each component is a TOML document with blocks as top-level sections and variables as entries within each section. Variables may be scalar or composite — composites contain fields (if records), choices (if unions), or items (if lists). The MODFLOW 6 repository stores per-component TOML files alongside the legacy `.dfn` files. +- **Legacy DFN format** (`.dfn`): flat text with comments demarcating blocks, used by MODFLOW 6 releases. +- **TOML format** (`.toml`): per-component TOML documents, produced by the `dfn2toml` conversion tool. -Both formats are supported by `modflow_devtools.dfns`. The v2 schema (TOML) is the canonical target format; legacy `.dfn` files can be mapped to v2 schema with `map()`. +**Schema version** describes the structure and semantics of the content: + +- **v1 schema**: the original structure embedded in legacy `.dfn` files. Mixes structural definitions with input format details (e.g., `in_record`, `tagged`). +- **v2 schema**: a cleaner, hierarchical representation. Each component has explicitly typed, nested fields; blocks and records are first-class objects; structural specification is separated from input format concerns. + +`modflow_devtools.dfns` always works with v2 schema objects internally. When loading a directory of `.dfn` files, they are parsed as v1 and automatically mapped to v2. TOML files carry v2 content directly and are loaded without mapping. Both file formats are supported by `Dfns.load()`. ### Core classes -#### `Dfn` +#### `Dfns` -Represents a single MODFLOW 6 input component (e.g. `gwf-chd`, `sim-nam`). A dataclass with attributes including `name`, `schema_version`, `blocks`, `parent`, `advanced`, `multi`, `subcomponents`, and optionally `children` (when part of a tree). +`Dfns` is a Pydantic model representing the full set of component definitions for a release. It is the primary object returned by `Dfns.load()` (see also [Registry](#registry) below, which loads and caches DFN files from remote releases). ```python -from modflow_devtools.dfns import DfnSpec +from modflow_devtools.dfns import Dfns + +# Load all component definitions from a directory +spec = Dfns.load("/path/to/mf6/doc/mf6io/mf6ivar/dfn") + +spec.schema_version # e.g. "2" +spec.root # the Simulation component, or None +len(spec.components) # total number of components -# Load a single component from a TOML file -with open("gwf-chd.toml", "rb") as f: - dfn = load(f, format="toml") +# Dict-like access to components +gwf_chd = spec.components["gwf-chd"] +gwf_chd.name # "gwf-chd" +gwf_chd.parent # "gwf-nam" -print(dfn.name) # "gwf-chd" -print(dfn.schema_version) # Version('2') -print(list(dfn.blocks)) # ['options', 'dimensions', 'period'] +# Navigate the component hierarchy +sim_children = spec.children_of("sim-nam") # {"gwf-nam": ..., ...} +gwf_children = spec.children_of("gwf-nam") # {"gwf-chd": ..., "gwf-wel": ..., ...} ``` -#### `DfnSpec` +#### Component types -Represents the full MODFLOW 6 input specification. Implements the `Mapping` protocol for flat dict-like access to components by name, and exposes the root component (simulation) with the full component hierarchy via `.root`. +Each entry in `spec.components` is one of three component types, discriminated by a `type` field: + +- `Simulation` — the root component (`sim-nam`) +- `Model` — a hydrologic process model (e.g. `gwf-nam`, `gwt-nam`) +- `Package` — any other input component (e.g. `gwf-chd`, `gwf-wel`) ```python -from modflow_devtools.dfns import DfnSpec +from modflow_devtools.dfns import Simulation, Model, Package + +gwf_nam = spec.components["gwf-nam"] +assert isinstance(gwf_nam, Model) + +gwf_chd = spec.components["gwf-chd"] +assert isinstance(gwf_chd, Package) +assert gwf_chd.multi is False +assert gwf_chd.subtype == "stress" +``` + +#### Blocks and fields -# Load from a directory of DFN files (legacy or TOML) -spec = DfnSpec.load("/path/to/mf6/doc/mf6io/mf6ivar/dfn") +Each component has `blocks`, a dict mapping block names to `Block` objects. Each `Block` has a `fields` dict of typed field objects. -# Hierarchical access -spec.root.name # "sim-nam" -spec.root.children["gwf-nam"] # GWF model name file Dfn -spec.root.children["gwf-nam"].children["gwf-chd"] # GWF CHD package Dfn +```python +from modflow_devtools.dfns import Block, Keyword, Double, List, Record -# Flat dict-like access -gwf_chd = spec["gwf-chd"] -for name, dfn in spec.items(): - print(name) -len(spec) # total number of components +period = gwf_chd.blocks["period"] +assert period.repeats is True -# Serialize the full spec as a single TOML document -with open("mf6spec.toml", "wb") as f: - spec.dump(f) +spd = period.fields["stress_period_data"] +assert isinstance(spd, List) +assert isinstance(spd.item, Record) -toml_str = spec.dumps() +cellid = spd.item.fields["cellid"] +assert isinstance(cellid, Array) ``` +Available field types: + +| Class | `type` value | Description | +|---|---|---| +| `Keyword` | `"keyword"` | Boolean presence/absence | +| `String` | `"string"` | String value | +| `Integer` | `"integer"` | Integer value | +| `Double` | `"double"` | Floating-point value | +| `File` | `"file"` (legacy `"path"`) | File path | +| `Array` | `"array"` | Fixed or dynamic array | +| `Record` | `"record"` | Single-line product type | +| `Union` | `"union"` | Tagged sum type | +| `List` | `"list"` | Tabular collection | + +See [DFN specification](dfnspec.md) for full attribute documentation. + ### Registry -The registry system handles discovering, caching, and accessing DFN files from MODFLOW 6 releases. Only released versions are supported by `RemoteDfnRegistry`; for working with unreleased or local DFN files, use `LocalDfnRegistry`. +The registry system handles caching and accessing DFN files from MODFLOW 6 releases. #### `LocalDfnRegistry` -For working with DFN files on the local filesystem. This is the right choice when working with a local MODFLOW 6 checkout, a CI environment with DFN files checked out, or any directory of DFN files not associated with a published release. +For working with DFN files on the local filesystem. ```python from modflow_devtools.dfns import LocalDfnRegistry registry = LocalDfnRegistry(path="/path/to/mf6/doc/mf6io/mf6ivar/dfn") -dfn = registry.get_dfn("gwf-chd") -spec = registry.spec +spec = registry.spec # Dfns instance +path = registry.get_path("gwf-chd") # Path to the component file ``` #### `RemoteDfnRegistry` -For fetching and caching DFN files from a MODFLOW 6 release. On first access for a given version, downloads the `mf{version}_dfns.zip` release asset from GitHub, extracts it to a local cache directory, and uses it for all subsequent access. Only accepts released version strings (e.g. `"6.6.0"`), not branch names or arbitrary git refs. +For fetching and caching DFN files from a MODFLOW 6 release. The `release_id` takes the form `"owner/repo@tag"`, where `tag` may be a specific version or `"latest"`. ```python from modflow_devtools.dfns import RemoteDfnRegistry -registry = RemoteDfnRegistry(source="modflow6", ref="6.6.0") -registry.sync() # downloads and caches DFN files for 6.6.0 +registry = RemoteDfnRegistry(release_id="MODFLOW-ORG/modflow6@6.6.0") +registry.sync() # download and cache DFN files +registry.sync(force=True) # force re-download -dfn = registry.get_dfn("gwf-chd") -spec = registry.spec -``` +spec = registry.spec # Dfns (auto-syncs if needed) +path = registry.get_path("gwf-chd") # Path to cached component file -#### Convenience functions - -```python -from modflow_devtools.dfns import ( - get_dfn, - get_dfn_path, - get_registry, - list_components, - list_releases, - sync_dfns, -) +tag = registry.latest_tag() # resolve "latest" to actual tag (network) +tag = registry.cached_tag() # return cached tag (no network) +``` -# List available releases -releases = list_releases() # e.g. ["6.6.0", "6.5.0", "6.4.4"] +For `@latest`, `latest_tag()` queries the GitHub API once and caches the result. -# Sync all available releases -sync_dfns() +#### Default registries -# Sync a specific release -sync_dfns(ref="6.6.0") +The package ships with a built-in configuration (`modflow_devtools/dfns/dfns.toml`) that lists the default release IDs to track: -# Get a component (auto-syncs if MODFLOW_DEVTOOLS_AUTO_SYNC=1) -dfn = get_dfn("gwf-chd", ref="6.6.0") +```toml +releases = [ + "MODFLOW-ORG/modflow6@latest", + "MODFLOW-ORG/modflow6-nightly-build@latest", +] +``` -# Get the local cached path to a component file -path = get_dfn_path("gwf-wel", ref="6.6.0") +To load these defaults: -# List all components for a release -components = list_components(ref="6.6.0") +```python +registries = RemoteDfnRegistry.load_default() +# {"MODFLOW-ORG/modflow6@latest": RemoteDfnRegistry(...), ...} +``` -# Get a registry object for a release -registry = get_registry(ref="6.6.0") +To load specific release IDs programmatically: -# Use a local path instead of a remote release -registry = get_registry(path="/path/to/dfns") -dfn = get_dfn("gwf-chd", path="/path/to/dfns") +```python +registries = RemoteDfnRegistry.from_ids( + "MODFLOW-ORG/modflow6@6.6.0", + "MODFLOW-ORG/modflow6@6.5.0", +) ``` -#### CLI +#### User config overlay -```shell -# List available releases -python -m modflow_devtools.dfns releases +You can extend or override the default registry configuration by creating: -# Sync all available releases -python -m modflow_devtools.dfns sync +- Linux/macOS: `~/.config/modflow-devtools/dfns.toml` (respects `$XDG_CONFIG_HOME`) +- Windows: `%APPDATA%/modflow-devtools/dfns.toml` -# Sync a specific release -python -m modflow_devtools.dfns sync --ref 6.6.0 +The file uses the same format as the bundled config: -# Force re-download -python -m modflow_devtools.dfns sync --force +```toml +releases = [ + "my-org/my-mf6-fork@main", +] +``` -# Show sync status and cache info -python -m modflow_devtools.dfns info +Entries in the user config are merged with (and take precedence over) the defaults. -# List available components for a release -python -m modflow_devtools.dfns list --ref 6.6.0 +#### Cache location -# Clear cache -python -m modflow_devtools.dfns clean -python -m modflow_devtools.dfns clean --all -``` +Downloaded DFN files are cached under: -#### Auto-sync +- Linux/macOS: `$XDG_CACHE_HOME/modflow-devtools/dfns/` (default `~/.cache/`) +- Windows: `%LOCALAPPDATA%/modflow-devtools/dfns/` -Auto-sync is opt-in (off by default). Enable it by setting the environment variable: +The cache is organized by repository and release tag: -```shell -MODFLOW_DEVTOOLS_AUTO_SYNC=1 +``` +~/.cache/modflow-devtools/dfns/ +└── MODFLOW-ORG/ + └── modflow6/ + ├── 6.6.0/ + │ ├── sim-nam.dfn + │ ├── gwf-chd.dfn + │ └── ... + └── 6.5.0/ + └── ... ``` -When enabled, `get_registry()` will automatically sync if no cached files exist for the requested release. +To get the base cache path programmatically: -#### Cache location +```python +RemoteDfnRegistry.base_cache_path() +``` -Downloaded DFN files are cached under: +#### Checking cache status -``` -~/.cache/modflow-devtools/dfns/ -└── modflow6/ - ├── 6.6.0/ - │ ├── sim-nam.toml - │ ├── gwf-chd.toml - │ └── ... - └── 6.5.0/ - ├── sim-nam.dfn - ├── gwf-chd.dfn - └── ... +```python +from modflow_devtools.dfns.registry import is_cached + +is_cached("MODFLOW-ORG/modflow6@6.6.0") # True/False (no network) ``` -### Schema versioning and mapping +#### Auto-sync -`modflow_devtools.dfns` supports multiple schema versions simultaneously: +When `MODFLOW_DEVTOOLS_AUTO_SYNC=1` is set, `RemoteDfnRegistry.from_ids()` will automatically call `sync()` for any release ID that has no cached files yet. -- **v1**: Original MODFLOW 6 releases. Mixes structural specification with input format details. Serialized as `.dfn` files. -- **v1.1**: Cleaned-up v1 with normalized attributes, structural improvements, and better parent-child inference. Can be serialized as `.dfn` or `.toml`. -- **v2**: Current TOML schema. Separates structural specification from input format concerns. Per-component `.toml` files in the MODFLOW 6 repository use this schema. +### CLI -Use `map()` to convert between schema versions: +```shell +# Show sync status for all configured releases +python -m modflow_devtools.dfns info -```python -from modflow_devtools.dfns import get_dfn, map +# Sync all configured releases (downloads dfns.zip from GitHub releases) +python -m modflow_devtools.dfns sync -dfn_v1 = get_dfn("gwf-chd", ref="6.4.4") # v1 schema -dfn_v2 = map(dfn_v1, schema_version="2") # convert to v2 -``` +# Force re-download even if already cached +python -m modflow_devtools.dfns sync --force -`DfnSpec.load()` automatically maps v1 DFNs to v2 when loading from a directory of legacy `.dfn` files. +# Clean the entire DFN cache +python -m modflow_devtools.dfns clean +``` diff --git a/modflow_devtools/dfn/__init__.py b/modflow_devtools/dfn/__init__.py index 64aeaf4a..d2c86cb1 100644 --- a/modflow_devtools/dfn/__init__.py +++ b/modflow_devtools/dfn/__init__.py @@ -1,6 +1,7 @@ import shutil import tempfile from os import PathLike +from pathlib import Path from modflow_devtools.dfn.schema import ( Dfn, @@ -37,7 +38,7 @@ def fetch_dfns(owner: str, repo: str, ref: str, outdir: str | PathLike, verbose: if verbose: print(f"Downloading MODFLOW 6 repository from {url}") with tempfile.TemporaryDirectory() as tmp: - dl_path = download_and_unzip(url, tmp, verbose=verbose) + dl_path = download_and_unzip(url, Path(tmp), verbose=verbose) contents = list(dl_path.glob("modflow6-*")) proj_path = next(iter(contents), None) if not proj_path: diff --git a/modflow_devtools/dfn/mapper.py b/modflow_devtools/dfn/mapper.py index a66a0f73..31f29e06 100644 --- a/modflow_devtools/dfn/mapper.py +++ b/modflow_devtools/dfn/mapper.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from modflow_devtools.dfn import schema as v1 @@ -12,16 +10,16 @@ def map_field(field: v1.Field) -> v1.Field: return v1.Field( name=field["name"], type=field["type"], - block=field["block"], - default=field["default"], - longname=field["longname"], - description=field["description"], - optional=field["optional"], - developmode=field["developmode"], - shape=field["shape"], - valid=field["valid"], - netcdf=field["netcdf"], - tagged=field["tagged"], + block=field.get("block"), + default=field.get("default"), + longname=field.get("longname"), + description=field.get("description"), + optional=field.get("optional", False), + developmode=field.get("developmode", False), + shape=field.get("shape"), + valid=field.get("valid"), + netcdf=field.get("netcdf", False), + tagged=field.get("tagged", False), ) @@ -33,13 +31,13 @@ def map(dfn: v1.Dfn) -> v1.Dfn: blocks[block_name] = { field_name: map_field(field) for field_name, field in block_fields.items() - if isinstance(field, v1.Field) + if isinstance(field, dict) } for block_name, block_fields in blocks.items(): - dfn.setdefault(block_name, {}) + dfn.setdefault(block_name, {}) # type: ignore[misc] for field_name, field_data in block_fields.items(): - dfn[block_name][field_name] = field_data + dfn[block_name][field_name] = field_data # type: ignore[literal-required] dfn["blocks"] = blocks if blocks else None dfn["schema_version"] = "1.1" diff --git a/modflow_devtools/dfn/schema.py b/modflow_devtools/dfn/schema.py index b3179eeb..d89c7819 100644 --- a/modflow_devtools/dfn/schema.py +++ b/modflow_devtools/dfn/schema.py @@ -13,6 +13,7 @@ from typing import ( Any, Literal, + NotRequired, TypedDict, ) from warnings import warn @@ -129,30 +130,28 @@ class Field(TypedDict): name: str type: FieldType - block: str | None = None - default: Any | None = None - longname: str | None = None - description: str | None = None - optional: bool = False - developmode: bool = False - shape: str | None = None - valid: tuple[str, ...] | None = None - netcdf: bool = False - tagged: bool = False - reader: Reader = "urword" - in_record: bool = False - layered: bool | None = None - preserve_case: bool = False - numeric_index: bool = False - deprecated: bool = False - removed: bool = False - mf6internal: str | None = None - block_variable: bool = False - just_data: bool = False - time_series: bool = False - - # for composite fields - children: Mapping[str, "Field"] = None + block: NotRequired[str | None] + default: NotRequired[Any | None] + longname: NotRequired[str | None] + description: NotRequired[str | None] + optional: NotRequired[bool] + developmode: NotRequired[bool] + shape: NotRequired[str | None] + valid: NotRequired[tuple[str, ...] | None] + netcdf: NotRequired[bool] + tagged: NotRequired[bool] + reader: NotRequired[Reader] + in_record: NotRequired[bool] + layered: NotRequired[bool | None] + preserve_case: NotRequired[bool] + numeric_index: NotRequired[bool] + deprecated: NotRequired[bool] + removed: NotRequired[bool] + mf6internal: NotRequired[str | None] + block_variable: NotRequired[bool] + just_data: NotRequired[bool] + time_series: NotRequired[bool] + children: NotRequired[Mapping[str, "Field"] | None] Fields = Mapping[str, "Field"] @@ -241,18 +240,18 @@ class Dfn(TypedDict): schema_version: str name: str - ftype: str | None = None - parent: str | list[str] | None = None - blocks: Blocks | None = None - children: Dfns | None = None - advanced: bool = False - multi: bool = False - ref: Ref | None = None - sln: Sln | None = None - fkeys: Dfns | None = None # deprecated - subcomponents: list[str] | None = None - - @staticmethod + ftype: NotRequired[str | None] + parent: NotRequired[str | list[str] | None] + blocks: NotRequired[Blocks | None] + children: NotRequired[Dfns | None] + advanced: NotRequired[bool] + multi: NotRequired[bool] + ref: NotRequired[Ref | None] + sln: NotRequired[Sln | None] + fkeys: NotRequired[Dfns | None] # deprecated + subcomponents: NotRequired[list[str] | None] + + @staticmethod # type: ignore[misc] def _load_v1_flat(f, common: dict | None = None) -> tuple[Mapping, list[str]]: field = {} flat = [] @@ -326,7 +325,7 @@ def _load_v1_flat(f, common: dict | None = None) -> tuple[Mapping, list[str]]: # the point of the OMD is to losslessly handle duplicate variable names return OMD(flat), meta - @classmethod + @classmethod # type: ignore[misc] def _load_v1(cls, f, name, **kwargs) -> "Dfn": """ Temporary load routine for the v1 DFN format. @@ -601,7 +600,7 @@ def _subcomponents() -> list[str] | None: result.append(abbr) return result if result else None - return cls( + return cls( # type: ignore[misc] name=name, fkeys=fkeys, advanced=_advanced(), @@ -612,14 +611,14 @@ def _subcomponents() -> list[str] | None: **blocks, ) - @classmethod + @classmethod # type: ignore[misc] def _load_v2(cls, f, name) -> "Dfn": data = tomli.load(f) if name and name != data.get("name", None): raise ValueError(f"Name mismatch, expected {name}") return cls(**data) - @classmethod + @classmethod # type: ignore[misc] def load( cls, f, @@ -638,7 +637,7 @@ def load( else: raise ValueError(f"Unsupported version, expected one of {version.__args__}") - @staticmethod + @staticmethod # type: ignore[misc] def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: """Load all component definitions from the given directory.""" @@ -709,7 +708,7 @@ def load(f: Any, format: str = "dfn", **kwargs: Any) -> Dfn: fields, meta = parser.parse_dfn(f, **kwargs) parent = parser.try_get_parent(meta) blocks = { - block_name: {field["name"]: Field(field) for field in block} + block_name: {field["name"]: Field(field) for field in block} # type: ignore[misc] for block_name, block in groupby(fields.values(multi=True), lambda fd: fd["block"]) } multi = parser.is_multi_package(meta) @@ -782,7 +781,10 @@ def resolve_parents(dfns: Dfns) -> Dfns: def to_tree(dfns: Dfns) -> Dfn: """Condense flat definitions to a hierarchical definition.""" - if (first_dfn := next(iter(dfns.values()), None))["schema_version"] != "1": + first_dfn = next(iter(dfns.values()), None) + if first_dfn is None: + raise ValueError("No definitions found") + if first_dfn["schema_version"] != "1": raise ValueError(f"Expected schema version 1, got {first_dfn['schema_version']!r}") dfns = resolve_parents(dfns) diff --git a/modflow_devtools/dfn2toml.py b/modflow_devtools/dfn2toml.py index 3f10182a..ba2e982d 100644 --- a/modflow_devtools/dfn2toml.py +++ b/modflow_devtools/dfn2toml.py @@ -85,10 +85,10 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str if schema_version == "1": pass # nothing to do elif schema_version == "1.1": - dfns = {name: map_v1_1(dfn, "1.1") for name, dfn in dfns.items()} dfns = v1.to_flat(v1.to_tree(dfns)) + dfns = {name: map_v1_1(dfn) for name, dfn in dfns.items()} elif schema_version == "2": - dfns = {name: map_v2(dfn, "2") for name, dfn in dfns.items()} + dfns = {name: map_v2(dfn) for name, dfn in dfns.items()} else: raise ValueError( f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" diff --git a/modflow_devtools/dfns/__main__.py b/modflow_devtools/dfns/__main__.py index 6862cabb..383e5d7f 100644 --- a/modflow_devtools/dfns/__main__.py +++ b/modflow_devtools/dfns/__main__.py @@ -7,13 +7,11 @@ python -m modflow_devtools.dfns clean """ -from __future__ import annotations - import argparse import shutil import sys -from modflow_devtools.dfns.registry import RemoteDfnRegistry, is_cached +from modflow_devtools.dfns.registry import RemoteDfnRegistry def cmd_sync(args: argparse.Namespace) -> int: @@ -30,7 +28,7 @@ def cmd_sync(args: argparse.Namespace) -> int: ) print(f" {registry.release_id}: {n_files} files") print(f"Synced {registry.release_id}") - return 0 + return 0 except Exception as e: print(f"Error: {e}", file=sys.stderr) return 1 @@ -42,10 +40,13 @@ def cmd_info(args: argparse.Namespace) -> int: try: for registry in registries.values(): - if is_cached(registry.release_id): - print(f"Cached: {registry.release_id}") + cached = registry.cached_tag() + if cached: + _, tag = registry.release_id.split("@") + suffix = f" ({cached})" if tag == "latest" else "" + print(f"Cached: {registry.release_id}{suffix}") else: - print(f"Uncached: {registry.release_id}") + print(f"Not cached: {registry.release_id}") return 0 except Exception as e: print(f"Error: {e}", file=sys.stderr) diff --git a/modflow_devtools/dfns/dfns.toml b/modflow_devtools/dfns/dfns.toml index 5680809c..7458bfd0 100644 --- a/modflow_devtools/dfns/dfns.toml +++ b/modflow_devtools/dfns/dfns.toml @@ -4,5 +4,6 @@ # - Windows: %APPDATA%/modflow-devtools/dfns.toml releases = [ - "MODFLOW-ORG/modflow6-nightly-build@20260518" + "MODFLOW-ORG/modflow6@latest", + "MODFLOW-ORG/modflow6-nightly-build@latest" ] diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index 41ecac5c..17ba2118 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -1,6 +1,5 @@ -from __future__ import annotations - import re +from collections.abc import Mapping from typing import Any, Literal from modflow_devtools.dfn import schema as v1 @@ -10,104 +9,92 @@ _IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") _LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") -_DIMS: frozenset[str] = frozenset( - {"nodes", "nlay", "nrow", "ncol", "ncpl", "nja", "ncelldim", "nvert"} -) +_SIM_PREFIXES: frozenset[str] = frozenset({"sim", "sln", "exg", "utl"}) -def _resolve_dimensions(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: - dims_referenced: set[str] = set() # dims used in shape expressions - dims_from_array: set[str] = set() # self-sizing array dimensions - dims_explicit: set[str] = set(_DIMS) # integer dimension fields +def _model_type_of(name: str) -> str | None: + """Extract model type from a component name, e.g. 'gwf' from 'gwf-npf'.""" + prefix = name.split("-")[0] if "-" in name else name + return None if prefix in _SIM_PREFIXES else prefix - # shape expressions may reference dimensions from several providers: - # - explicitly defined integer dimension fields - # - dynamic dimensions: size of 1D arrays - def _scan_fields(fields: dict[str, v2.Field]) -> None: - for field in fields.values(): +def _build_explicit_dims(dfn_name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.DimDef]: + """Build the dims section from a component's dimensions block.""" + dims: dict[str, v2.DimDef] = {} + dim_block = blocks.get("dimensions") + if not dim_block: + return dims + + model_type = _model_type_of(dfn_name) + if dfn_name == "sim-tdis": + default_scope = "simulation" + elif model_type: + default_scope = model_type + else: + default_scope = "component" + + for fname, field in dim_block.fields.items(): + if isinstance(field, v2.Integer): + dims[fname] = v2.DimDef(field=fname, scope=default_scope) + + # Add derived dims for standard MF6 grid discretization types. + if model_type: + has = set(dims.keys()) + if {"nlay", "nrow", "ncol"} <= has: + dims["nodes"] = v2.DimDef(expr="nlay * nrow * ncol", scope=model_type) + dims["ncelldim"] = v2.DimDef(expr="3", scope=model_type) + elif {"nlay", "ncpl"} <= has: + dims["nodes"] = v2.DimDef(expr="nlay * ncpl", scope=model_type) + dims["ncelldim"] = v2.DimDef(expr="2", scope=model_type) + elif "nodes" in has: + dims["ncelldim"] = v2.DimDef(expr="1", scope=model_type) + + return dims + + +def _resolve_dimensions( + blocks: dict[str, v2.Block], +) -> tuple[dict[str, v2.Block], dict[str, v2.DimDef]]: + """ + Detect self-sizing arrays whose name is referenced in another array's shape + expression — those define a component-scoped dimension. + + Any array type qualifies (not just string). Returns the unchanged blocks + alongside a dict of component-scoped DimDef entries. + """ + self_sizing: set[str] = set() + shape_refs: set[str] = set() + + def _scan(fields: Mapping[str, v2.Field]) -> None: + for name, field in fields.items(): if isinstance(field, v2.Array): - for dim in field.shape: - if _IDENT_RE.fullmatch(dim): - dims_referenced.add(dim) + if not field.shape: + self_sizing.add(name) else: - # no shape expr, self-sizing - dims_from_array.add(field.name) - scope = getattr(field, "dimension", None) - if scope in ("component", "model", "simulation"): - dims_explicit.add(field.name) + for elem in field.shape: + if _IDENT_RE.fullmatch(elem): + shape_refs.add(elem) if isinstance(field, v2.Record): - _scan_fields(field.fields) + _scan(field.fields) elif isinstance(field, v2.Union): - _scan_fields(field.arms) + _scan(field.arms) elif isinstance(field, v2.List): item = field.item - _scan_fields(item.fields if isinstance(item, v2.Record) else item.arms) + _scan(item.fields if isinstance(item, v2.Record) else item.arms) for block in blocks.values(): - _scan_fields(block.fields) - - if not dims_referenced: - return blocks - - dims_provided: set[str] = dims_from_array & dims_explicit - - def _get_dims(record: v2.Record) -> set[str]: - found: set[str] = set() - for field in record.fields.values(): - if isinstance(field, v2.Array): - for dim in field.shape: - if _IDENT_RE.fullmatch(dim) and dim not in dims_provided: - # the shape expression of an array inside a record - # may reference a sibling integer subfield even if - # the integer is not marked as a dimension. - if (sibling := record.fields.get(dim, None)) is not None and isinstance( - sibling, v2.Integer - ): - found.add(dim) - return found - - def _resolve_fields(fields: dict[str, v2.Field]) -> dict[str, v2.Field]: - result = {} - for name, field in fields.items(): - if isinstance(field, v2.Array) and name in dims_provided: - field.dimension = "component" - elif isinstance(field, v2.Record): - local_dims = _get_dims(field) - subfields = _resolve_fields(field.fields) - if local_dims: - for subfield_name, subfield in subfields.items(): - if subfield_name in local_dims and isinstance(subfield, v2.Integer): - subfield.dimension = "record" - field.fields = subfields - elif isinstance(field, v2.Union): - field.arms = _resolve_fields(field.arms) - elif isinstance(field, v2.List): - if isinstance(field.item, v2.Record): - local_dims = _get_dims(field.item) - subfields = _resolve_fields(field.item.fields) - if local_dims: - for subfield_name, subfield in subfields.items(): - if subfield_name in local_dims and isinstance(subfield, v2.Integer): - subfield.dimension = "record" - field.item.fields = subfields - else: - field.item.arms = _resolve_fields(field.item.arms) - result[name] = field - return result - - def _resolve_block(block: v2.Block) -> v2.Block: - block.fields = _resolve_fields(block.fields) - return block + _scan(block.fields) - return {block_name: _resolve_block(block) for block_name, block in blocks.items()} + array_dim_names = self_sizing & shape_refs + array_dims = {n: v2.DimDef(field=n, scope="component") for n in array_dim_names} + return blocks, array_dims def _resolve_relations(blocks: dict[str, v2.Block]) -> dict[str, v2.Block]: pk_set: set[tuple[str, str]] = set() fk_map: dict[tuple[str, str], str] = {} - def _scan_fields(block_name: str, fields: dict[str, v2.Field]) -> None: + def _scan_fields(block_name: str, fields: Mapping[str, v2.Field]) -> None: def _scan_record(record: v2.Record) -> None: for field in record.fields.values(): @@ -138,7 +125,7 @@ def _scan_record(record: v2.Record) -> None: if not fk_map and not pk_set: return blocks - def _resolve_fields(block_name: str, fields: dict[str, v2.Field]) -> dict[str, v2.Field]: + def _resolve_fields(block_name: str, fields: Mapping[str, v2.Field]) -> dict[str, v2.Field]: def _resolve_record(record: v2.Record) -> v2.Record: updates: dict = {} @@ -161,16 +148,19 @@ def _resolve_record(record: v2.Record) -> v2.Record: if isinstance(f, v2.Record): f = _resolve_record(f) elif isinstance(f, v2.Union): - f.arms = _resolve_fields(block_name, f.arms) + f.arms = _resolve_fields(block_name, f.arms) # type: ignore[assignment] elif isinstance(f, v2.List): if isinstance(f.item, v2.Record): f.item = _resolve_record(f.item) else: - f.item.arms = _resolve_fields(block_name, f.item.arms) + f.item.arms = _resolve_fields(block_name, f.item.arms) # type: ignore[assignment] result[name] = f return result - return {block_name: _resolve_fields(block, block_name) for block_name, block in blocks.items()} + return { + block_name: block.model_copy(update={"fields": _resolve_fields(block_name, block.fields)}) + for block_name, block in blocks.items() + } def map(dfn: v1.Dfn) -> v2.Component: @@ -195,8 +185,7 @@ def _to_bool(v: Any, default: bool = False) -> bool: return default def __map_field(f: v1.Field) -> v2.Field: - fd = dict(f) - fd = {k: try_parse_bool(v) for k, v in fd.items()} + fd: dict[str, Any] = {k: try_parse_bool(v) for k, v in dict(f).items()} _name: str = fd["name"] _type: str | None = fd.get("type") @@ -239,7 +228,7 @@ def _parse_shape(s: str) -> list[str]: for fi in fields.values(multi=True) if fi["name"] == col_name and fi["type"] == "integer" - and fi["in_record"] + and fi.get("in_record", False) ), None, ) @@ -251,7 +240,7 @@ def _parse_shape(s: str) -> list[str]: fi["name"] for fi in fields.values(multi=True) if fi["type"] == "string" - and (fi["shape"] or "").strip() in (f"({elem})", elem) + and (fi.get("shape") or "").strip() in (f"({elem})", elem) ), None, ) @@ -286,17 +275,6 @@ def _to_scalar() -> v2.Scalar: ) if _type == "integer": v = [int(x) for x in valid] if valid else None - if fd.get("block") == "dimensions": - if _name in _DIMS: - _dim_scope: ( - Literal["record", "component", "model", "simulation"] | None - ) = "model" - elif dfn["name"] == "sim-tdis" and _name == "nper": - _dim_scope = "simulation" - else: - _dim_scope = "component" - else: - _dim_scope = None return v2.Integer( name=_name, longname=longname, @@ -308,7 +286,6 @@ def _to_scalar() -> v2.Scalar: tagged=tagged, valid=v, time_series=time_series, - dimension=_dim_scope, ) if _type in ("double", "double precision"): return v2.Double( @@ -332,7 +309,7 @@ def _row_field() -> v2.Record | v2.Union: item_types = [ fi["type"] for fi in fields.values(multi=True) - if fi["name"] in item_names and fi["in_record"] + if fi["name"] in item_names and fi.get("in_record", False) ] if ( @@ -364,7 +341,7 @@ def _row_field() -> v2.Record | v2.Union: children = { fi["name"]: __map_field(fi) for fi in fields.values(multi=True) - if fi["name"] in item_names and fi["in_record"] + if fi["name"] in item_names and fi.get("in_record", False) } first = next(iter(children.values())) if len(children) == 1 and isinstance(first, v2.Union): @@ -382,7 +359,7 @@ def _union_fields() -> dict: return { fi["name"]: __map_field(fi) for fi in fields.values(multi=True) - if fi["name"] in names and fi["in_record"] + if fi["name"] in names and fi.get("in_record", False) } def _record_fields() -> dict: @@ -393,7 +370,7 @@ def _record_fields() -> dict: fi for fi in fields.values(multi=True) if fi["name"] == rname - and fi["in_record"] + and fi.get("in_record", False) and not (fi["type"] or "").startswith("record") ] if matches: @@ -451,19 +428,10 @@ def _record_fields() -> dict: dtype = dtype_map.get(_type) if dtype is not None: if dtype == "string": - # If the v1 shape is a single count identifier that isn't - # an explicit integer field (e.g. naux for auxiliary), the - # array defines that dimension by its length. - _str_dim: Literal["component", "model", "simulation"] | None = None - _parsed_str = _parse_shape(shape_str) - if len(_parsed_str) == 1: - _count_name = _parsed_str[0] - _is_explicit_int = any( - fi["name"] == _count_name and fi["type"] == "integer" - for fi in fields.values(multi=True) - ) - if not _is_explicit_int: - _str_dim = "component" + # String arrays in v1 are always self-sizing; whether the + # array defines a component dimension is detected generically + # by _resolve_dimensions (any self-sizing array referenced + # by name in a sibling shape expression is a dim source). return v2.Array( name=_name, longname=longname, @@ -475,7 +443,6 @@ def _record_fields() -> dict: time_series=time_series, dtype="string", shape=[], - dimension=_str_dim, ) parsed_shape = _parse_shape(shape_str) return v2.Array( @@ -499,7 +466,7 @@ def _record_fields() -> dict: blocks: dict[str, v2.Block] = {} for field in fields.values(multi=True): - if field["in_record"]: # type: ignore[attr-defined] + if field.get("in_record", False): continue # record subfields are handled recursively v2_field = _map_field(field) blocks.setdefault(field["block"], v2.Block(name=field["block"], fields={})).fields[ @@ -507,14 +474,17 @@ def _record_fields() -> dict: ] = v2_field blocks[field["block"]].repeats = field.get("block_variable", False) - blocks = _resolve_dimensions(blocks) + blocks, array_dims = _resolve_dimensions(blocks) blocks = _resolve_relations(blocks) + explicit_dims = _build_explicit_dims(name, blocks) + dims = {**explicit_dims, **array_dims} or None d: dict[str, Any] = { "schema_version": "2", "name": name, "parent": dfn["parent"], "blocks": blocks or None, + "dims": dims, } if name == "sim-nam": return v2.Simulation(**d) diff --git a/modflow_devtools/dfns/registry.py b/modflow_devtools/dfns/registry.py index 33a21fc4..f02b3ca7 100644 --- a/modflow_devtools/dfns/registry.py +++ b/modflow_devtools/dfns/registry.py @@ -1,7 +1,7 @@ -from __future__ import annotations - +import json import os import tempfile +import urllib.request from os import PathLike from pathlib import Path from platform import system @@ -68,6 +68,20 @@ class RemoteDfnRegistry(DfnRegistry): description="DFN source repository release ID (owner/name@tag)", ) + _latest: str | None = PrivateAttr(default=None, init=False) + + def latest_tag(self) -> str: + repo, tag = self.release_id.split("@") + if tag != "latest": + return tag + if self._latest is None: + owner, name = repo.split("/") + with urllib.request.urlopen( + f"https://api.github.com/repos/{owner}/{name}/releases/latest" + ) as resp: + self._latest = json.loads(resp.read())["tag_name"] + return self._latest + @staticmethod def base_cache_path() -> Path: """ @@ -96,7 +110,7 @@ def user_config_path() -> Path: return base / "modflow-devtools" / "dfns.toml" @classmethod - def from_ids(cls, *ids: str) -> dict[str, RemoteDfnRegistry]: + def from_ids(cls, *ids: str) -> "dict[str, RemoteDfnRegistry]": """Create registries from one or more DFN source repository release IDs.""" registries = {} @@ -113,7 +127,7 @@ def from_ids(cls, *ids: str) -> dict[str, RemoteDfnRegistry]: return registries @classmethod - def load(cls, path: str | PathLike) -> dict[str, RemoteDfnRegistry]: + def load(cls, path: str | PathLike) -> "dict[str, RemoteDfnRegistry]": """Load registries from a TOML file of DFN source repository release IDs.""" path = Path(path) if not path.exists(): @@ -130,7 +144,7 @@ def load(cls, path: str | PathLike) -> dict[str, RemoteDfnRegistry]: return registries @classmethod - def load_default(cls) -> dict[str, RemoteDfnRegistry]: + def load_default(cls) -> "dict[str, RemoteDfnRegistry]": """ Load registries from remote DFN source repository configuration bundled with the package, and from a user overlay configuration file if present. @@ -144,8 +158,8 @@ def load_default(cls) -> dict[str, RemoteDfnRegistry]: @property def cache_path(self) -> Path: - repo, tag = self.release_id.split("@") - return RemoteDfnRegistry.base_cache_path() / repo / tag + repo, _ = self.release_id.split("@") + return RemoteDfnRegistry.base_cache_path() / repo / self.latest_tag() @property def spec(self) -> Dfns: @@ -162,8 +176,8 @@ def sync(self, force: bool = False) -> None: return asset_name = "dfns.zip" - repo, tag = self.release_id.split("@") - url = f"https://github.com/{repo}/releases/download/{tag}/{asset_name}" + repo, _ = self.release_id.split("@") + url = f"https://github.com/{repo}/releases/download/{self.latest_tag()}/{asset_name}" self.cache_path.mkdir(parents=True, exist_ok=True) @@ -176,6 +190,25 @@ def sync(self, force: bool = False) -> None: processor=pooch.Unzip(extract_dir=str(self.cache_path)), ) + def cached_tag(self) -> str | None: + """ + Return the cached tag for this release without making a network request. + + For exact tags, checks the specific cache directory. For ``@latest``, + scans the repo's cache directory and returns the most recently modified + cached tag, or None if nothing is cached. + """ + repo, tag = self.release_id.split("@") + if tag != "latest": + return tag if self.cache_path.exists() and any(self.cache_path.iterdir()) else None + repo_cache = RemoteDfnRegistry.base_cache_path() / repo + if not repo_cache.is_dir(): + return None + tags = [p for p in repo_cache.iterdir() if p.is_dir() and any(p.iterdir())] + if not tags: + return None + return max(tags, key=lambda p: p.stat().st_mtime).name + def get_path(self, component: str) -> Path: if not self.cache_path.exists() or not any(self.cache_path.iterdir()): self.sync() @@ -190,6 +223,6 @@ def is_cached(release_id: str) -> bool: """ Check whether a remote DFN source repository's release is in the cache. """ - repo, tag = release_id.split("@") - cache_dir = RemoteDfnRegistry.base_cache_path() / repo / tag + registry = RemoteDfnRegistry(release_id=release_id) + cache_dir = registry.cache_path return any(cache_dir.iterdir()) if cache_dir.is_dir() else False diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index fe134994..56801090 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -9,7 +9,6 @@ from pydantic import ( BaseModel, computed_field, - field_validator, model_validator, ) from pydantic import ( @@ -82,21 +81,11 @@ class Integer(FieldBase): netcdf: bool = False tagged: bool = True valid: list[int] | None = None - dimension: Literal["record", "component", "model", "simulation"] | None = None time_series: bool = False pk: bool = False fk: str | None = None fk_ref: str | None = None - @field_validator("dimension", mode="before") - @classmethod - def _coerce_dimension(cls, v: Any) -> Any: - if v is True: - return "component" - if v is False: - return None - return v - class Double(FieldBase): type: Literal["double"] = PydanticField(default="double", frozen=True) @@ -141,26 +130,6 @@ class Array(FieldBase): shape: list[str] = [] time_series: bool = False repeat: str | None = None - dimension: Literal["component", "model", "simulation"] | None = None - - @field_validator("dimension", mode="before") - @classmethod - def _coerce_dimension(cls, v: Any) -> Any: - if v is True: - return "component" - if v is False: - return None - return v - - @model_validator(mode="after") - def _validate_dimension(self) -> "Array": - if self.dimension is not None and self.shape: - raise ValueError( - f"Array {self.name!r}: the 'dimension' attribute may only " - "be set on self-sizing arrays (i.e., arrays whose 'shape' " - "is empty)" - ) - return self class Record(FieldBase): @@ -220,37 +189,6 @@ def children(self) -> "dict[str, Field]": List.model_rebuild() -DIMENSION_SCOPES = ("component", "model", "simulation") - - -def _collect_explicit_dims(component: "ComponentBase") -> set[str]: - """ - Gather all explicitly declared dimension names from a component. - - Collects integer or array fields with a ``dimension`` attribute, - recursing into Records, Union arms, and List items as necessary. - """ - dims: set[str] = set() - - def _scan(fields: "dict[str, Any]") -> None: - for f in fields.values(): - if isinstance(f, Integer) and f.dimension in DIMENSION_SCOPES: - dims.add(f.name) - elif isinstance(f, Array) and f.dimension in DIMENSION_SCOPES: - dims.add(f.name) - elif isinstance(f, Record): - _scan(f.fields) - elif isinstance(f, Union): - _scan(f.arms) - elif isinstance(f, List): - item = f.item - _scan(item.fields if isinstance(item, Record) else item.arms) - - for block in (component.blocks or {}).values(): - _scan(block.fields) - return dims - - def _names_in_expr(expr: str) -> set[str]: """Return Name identifiers from expr, excluding those inside sum() calls.""" try: @@ -320,27 +258,55 @@ def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> ) +class DimDef(BaseModel): + """A named dimension, either backed by a field or derived from an expression.""" + + field: str | None = None # name of the field that provides this dimension + expr: str | None = None # derivation expression, e.g. "nlay * nrow * ncol" + scope: str # "component" | "model" | "simulation" | "" + + @model_validator(mode="after") + def _check_exclusive(self) -> "DimDef": + if (self.field is None) == (self.expr is None): + raise ValueError("DimDef must have exactly one of 'field' or 'expr'") + return self + + @property + def is_derived(self) -> bool: + return self.expr is not None + + +_SIM_PREFIXES: frozenset[str] = frozenset({"sim", "sln", "exg", "utl"}) + + +def _model_type(name: str) -> str | None: + """Extract model type from a component name, e.g. 'gwf' from 'gwf-npf'.""" + prefix = name.split("-")[0] if "-" in name else name + return None if prefix in _SIM_PREFIXES else prefix + + def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> list[str]: """ - Validate derived_dims expressions and return their names in topological order. + Validate derived dims expressions and return their names in topological order. Raises ValueError on cycles or unresolvable operands. - ``known_dims`` is the full set of dim names visible to this component - (explicit + derived + inherited); pass ``_known_dims_for(spec, name)`` from - ``DfnSpec._validate_dims_and_shapes``, or an explicit set in tests. + ``known_dims`` is the full set of dim names visible to this component; + pass ``spec.dims(name)`` or an explicit set in tests. """ - derived = component.derived_dims or {} + derived = {n: d for n, d in (component.dims or {}).items() if d.is_derived} if not derived: return [] derived_names = set(derived.keys()) deps: dict[str, set[str]] = {} - for name, expr in derived.items(): + for name, dim_def in derived.items(): + expr = dim_def.expr + assert expr is not None try: tree = ast.parse(expr, mode="eval") except SyntaxError as e: - raise ValueError(f"Invalid derived_dims {name!r}: {expr!r}: {e}") from e + raise ValueError(f"Invalid dims {name!r}: {expr!r}: {e}") from e for node in ast.walk(tree): if ( @@ -353,7 +319,7 @@ def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> l operands = _names_in_expr(expr) for op in operands: if op not in known_dims and op not in derived_names: - raise ValueError(f"derived_dims {name!r} operand {op!r} is not a known dimension") + raise ValueError(f"dims {name!r} operand {op!r} is not a known dimension") deps[name] = operands & derived_names in_degree = dict.fromkeys(derived_names, 0) @@ -375,7 +341,7 @@ def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> l if len(order) != len(derived_names): cyclic = {n for n, d in in_degree.items() if d > 0} - raise ValueError(f"Cycle in derived_dims: {cyclic}") + raise ValueError(f"Cycle in dims: {cyclic}") return order @@ -398,7 +364,7 @@ class ComponentBase(BaseModel): blocks: dict[str, Block] | None = None parent: str | list[str] | None = None schema_version: str | None = None - derived_dims: dict[str, str] | None = None + dims: dict[str, DimDef] | None = None class Simulation(ComponentBase): @@ -427,22 +393,6 @@ class Package(ComponentBase): _ARITH_RE = re.compile(r"^([A-Za-z_]\w*)\s*[+-]\s*\d+$") -def _known_dims_for(spec: "Dfns", component_name: str) -> set[str]: - """ - Return the full set of dim names valid for shape references in a component. - Scope chain (levels 1-3; level 4 is intra-record sibling, checked per-field): - 1. local explicit dims: Integer(dimension=True) and Array(dtype="string", dimension=True) - 2. local derived dims (keys of component.derived_dims) - 3. inherited grid dims (from all other components in the spec) - """ - component = spec.components[component_name] - return ( - _collect_explicit_dims(component) - | set((component.derived_dims or {}).keys()) - | spec.grid_dims_for(component_name) - ) - - def _find_list_in_block(component: "ComponentBase", block_name: str) -> "List | None": """Return the first List field in the named block, or None.""" block = (component.blocks or {}).get(block_name) @@ -484,7 +434,7 @@ def _validate_shape_element( return if enclosing_record is not None: sibling = enclosing_record.fields.get(core) - if isinstance(sibling, Integer) and sibling.dimension == "record": + if isinstance(sibling, Integer): return raise ValueError( f"Array {array_field.name!r} shape element {element!r}: " @@ -498,7 +448,7 @@ def _validate_shape_element( # an inline count on the same line. if enclosing_record is not None: sibling = enclosing_record.fields.get(element) - if isinstance(sibling, Integer) and sibling.dimension == "record": + if isinstance(sibling, Integer): return raise ValueError( f"Array {array_field.name!r} shape element {element!r} " @@ -569,7 +519,7 @@ def _validate_shape_element( return if enclosing_record is not None: sibling = enclosing_record.fields.get(dim_name) - if isinstance(sibling, Integer) and sibling.dimension == "record": + if isinstance(sibling, Integer): return raise ValueError( f"Array {array_field.name!r} shape element {element!r}: " @@ -652,7 +602,7 @@ def _validate_array_shapes( if not component.blocks: return - known_dims = _known_dims_for(spec, component_name) + known_dims = spec.dims(component_name) def _check_array(arr: "Array", enclosing: "Record | None") -> None: if not arr.shape: @@ -693,7 +643,7 @@ class Dfns(BaseModel): components: dict[str, Component] = PydanticField(default_factory=dict) - @computed_field + @computed_field # type: ignore[prop-decorator] @property def schema_version(self) -> str: for c in self.components.values(): @@ -709,27 +659,44 @@ def root(self) -> "Simulation | None": return c return None - def children_of(self, name: str) -> "dict[str, Component]": - """Return all components whose parent matches ``name``.""" + def children(self, name: str) -> "dict[str, Component]": + """Components whose parent matches ``name``.""" return {n: c for n, c in self.components.items() if c.parent == name} - def explicit_dims_for(self, component_name: str) -> set[str]: - """Return the set of explicit dim names for a component.""" - return _collect_explicit_dims(self.components[component_name]) - - def grid_dims_for(self, component_name: str) -> set[str]: + def local_dims(self, component_name: str) -> set[str]: + """Dim names declared in this component's dims section.""" + return set((self.components[component_name].dims or {}).keys()) + + def inherited_dims(self, component_name: str) -> set[str]: + """Dim names visible to ``component_name`` from other components.""" + inherited: set[str] = set() + component = self.components[component_name] + for cname, c in self.components.items(): + if cname == component_name: + continue + for dim_name, dim in (c.dims or {}).items(): + match dim.scope: + case "simulation": + inherited.add(dim_name) + case "model": + if "model" in component.parent or cname in component.parent: + inherited.add(dim_name) + case "component": + if cname in component.parent: + inherited.add(dim_name) + return inherited + + def dims(self, component_name: str) -> set[str]: """ - Return dim names inherited by ``component_name`` from the rest of the spec. + Return all dim names visible to ``component_name`` for shape resolution. + + This is the union of the component's own declared dims (field-backed and + derived) and any dims inherited from other components via scoping rules. """ - dims: set[str] = set() - for name, c in self.components.items(): - if name != component_name: - dims |= _collect_explicit_dims(c) - dims |= set((c.derived_dims or {}).keys()) - return dims + return self.local_dims(component_name) | self.inherited_dims(component_name) @model_validator(mode="after") - def _validate_schema_version_consistency(self) -> "Dfns": + def _validate_schema_version(self) -> "Dfns": versions = { c.schema_version for c in self.components.values() if c.schema_version is not None } @@ -741,17 +708,10 @@ def _validate_schema_version_consistency(self) -> "Dfns": return self @model_validator(mode="after") - def _validate_dims_and_shapes(self) -> "Dfns": - """ - At construction time, for every component: - 1. Validate derived_dims expressions (topological sort, operand scope). - 2. Validate every Array.shape element (dim reference or row-level lookup). - Shape validation runs after dims so the derived dim names are available - as part of the known scope when checking dim references. - """ + def _validate_relations(self) -> "Dfns": for name, component in self.components.items(): - if component.derived_dims: - _resolve_derived_dims(component, _known_dims_for(self, name)) + if component.dims and any(d.is_derived for d in component.dims.values()): + _resolve_derived_dims(component, self.dims(name)) for name, component in self.components.items(): _validate_fk_fields(component, self) for name, component in self.components.items(): diff --git a/modflow_devtools/imports.py b/modflow_devtools/imports.py index 3ebcb42e..da39e8cc 100644 --- a/modflow_devtools/imports.py +++ b/modflow_devtools/imports.py @@ -33,8 +33,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from __future__ import annotations - import importlib import sys import types From 1c92ed832c62f9e46b2627a5615cbdc4f3d21011 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Thu, 21 May 2026 17:29:07 -0700 Subject: [PATCH 14/29] fixes --- autotest/dfn/test_dfn.py | 79 +++++++++++++-------- autotest/dfns/test_dfns_schema.py | 111 +++++++++++++++--------------- docs/md/dfnspec.md | 2 +- modflow_devtools/dfn/schema.py | 21 +++++- modflow_devtools/dfns/mapper.py | 59 +++++++++------- modflow_devtools/dfns/schema.py | 52 +++++++++++--- 6 files changed, 203 insertions(+), 121 deletions(-) diff --git a/autotest/dfn/test_dfn.py b/autotest/dfn/test_dfn.py index 0add292a..b10e493a 100644 --- a/autotest/dfn/test_dfn.py +++ b/autotest/dfn/test_dfn.py @@ -1,53 +1,71 @@ +import tempfile from pathlib import Path from modflow_devtools.dfn import Dfn, fetch_dfns from modflow_devtools.dfn2toml import convert from modflow_devtools.markers import requires_pkg -PROJ_ROOT = Path(__file__).parents[1] -DFN_DIR = PROJ_ROOT / "autotest" / "temp" / "dfn" -TOML_V1_DIR = DFN_DIR / "toml" -TOML_V1_1_DIR = DFN_DIR / "toml-v1_1" MF6_OWNER = "MODFLOW-ORG" MF6_REPO = "modflow6" MF6_REF = "develop" +_DFN_DIR: Path | None = None +_TOML_V1_DIR: Path | None = None +_TOML_V1_1_DIR: Path | None = None +_TMPDIR: tempfile.TemporaryDirectory | None = None + + +def _ensure_dfns() -> Path: + global _DFN_DIR, _TMPDIR + if _DFN_DIR is None: + _TMPDIR = tempfile.TemporaryDirectory() + _DFN_DIR = Path(_TMPDIR.name) / "dfn" + _DFN_DIR.mkdir() + fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, _DFN_DIR, verbose=True) + return _DFN_DIR + + +def _ensure_toml_v1() -> Path: + global _TOML_V1_DIR + dfn_dir = _ensure_dfns() + if _TOML_V1_DIR is None: + _TOML_V1_DIR = dfn_dir / "toml" + convert(dfn_dir, _TOML_V1_DIR, schema_version="1") + return _TOML_V1_DIR + + +def _ensure_toml_v1_1() -> Path: + global _TOML_V1_1_DIR + dfn_dir = _ensure_dfns() + if _TOML_V1_1_DIR is None: + _TOML_V1_1_DIR = dfn_dir / "toml-v1_1" + convert(dfn_dir, _TOML_V1_1_DIR, schema_version="1.1") + return _TOML_V1_1_DIR + def pytest_generate_tests(metafunc): if "dfn_name" in metafunc.fixturenames: - if not any(DFN_DIR.glob("*.dfn")): - fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, DFN_DIR, verbose=True) - dfn_names = [ - dfn.stem for dfn in DFN_DIR.glob("*.dfn") if dfn.stem not in ["common", "flopy"] - ] + dfn_dir = _ensure_dfns() + dfn_names = [p.stem for p in dfn_dir.glob("*.dfn") if p.stem not in ("common", "flopy")] metafunc.parametrize("dfn_name", dfn_names, ids=dfn_names) if "toml_name" in metafunc.fixturenames: - dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] - if not TOML_V1_DIR.exists() or not all( - (TOML_V1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths - ): - convert(DFN_DIR, TOML_V1_DIR) - assert all((TOML_V1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) - toml_names = [toml.stem for toml in TOML_V1_DIR.glob("*.toml")] + toml_dir = _ensure_toml_v1() + toml_names = [p.stem for p in toml_dir.glob("*.toml")] metafunc.parametrize("toml_name", toml_names, ids=toml_names) if "toml_v1_1_name" in metafunc.fixturenames: - dfn_paths = [p for p in DFN_DIR.glob("*.dfn") if p.stem not in ["common", "flopy"]] - if not TOML_V1_1_DIR.exists() or not all( - (TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths - ): - convert(DFN_DIR, TOML_V1_1_DIR, schema_version="1.1") - assert all((TOML_V1_1_DIR / f"{dfn.stem}.toml").is_file() for dfn in dfn_paths) - toml_names = [toml.stem for toml in TOML_V1_1_DIR.glob("*.toml")] + toml_dir = _ensure_toml_v1_1() + toml_names = [p.stem for p in toml_dir.glob("*.toml")] metafunc.parametrize("toml_v1_1_name", toml_names, ids=toml_names) @requires_pkg("boltons") def test_load_v1(dfn_name): + dfn_dir = _ensure_dfns() with ( - (DFN_DIR / "common.dfn").open() as common_file, - (DFN_DIR / f"{dfn_name}.dfn").open() as dfn_file, + (dfn_dir / "common.dfn").open() as common_file, + (dfn_dir / f"{dfn_name}.dfn").open() as dfn_file, ): common, _ = Dfn._load_v1_flat(common_file) dfn = Dfn.load(dfn_file, name=dfn_name, common=common) @@ -56,16 +74,16 @@ def test_load_v1(dfn_name): @requires_pkg("boltons") def test_load_v2(toml_name): - with (TOML_V1_DIR / f"{toml_name}.toml").open(mode="rb") as toml_file: + toml_dir = _ensure_toml_v1() + with (toml_dir / f"{toml_name}.toml").open(mode="rb") as toml_file: toml = Dfn.load(toml_file, name=toml_name, version=2) assert any(toml) @requires_pkg("boltons") def test_load_all(): - if not any(DFN_DIR.glob("*.dfn")): - fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, DFN_DIR, verbose=True) - dfns = Dfn.load_all(DFN_DIR) + dfn_dir = _ensure_dfns() + dfns = Dfn.load_all(dfn_dir) assert any(dfns) @@ -76,7 +94,8 @@ def test_convert_v1_1(toml_v1_1_name): except ImportError: import tomli as tomllib # type: ignore[no-redef] - with (TOML_V1_1_DIR / f"{toml_v1_1_name}.toml").open("rb") as f: + toml_dir = _ensure_toml_v1_1() + with (toml_dir / f"{toml_v1_1_name}.toml").open("rb") as f: data = tomllib.load(f) assert data["name"] == toml_v1_1_name assert data["schema_version"] == "1.1" diff --git a/autotest/dfns/test_dfns_schema.py b/autotest/dfns/test_dfns_schema.py index f6c4d003..9899e864 100644 --- a/autotest/dfns/test_dfns_schema.py +++ b/autotest/dfns/test_dfns_schema.py @@ -323,9 +323,9 @@ def test_local_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -516,10 +516,10 @@ def test_dfnspec_construction_validates_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -566,9 +566,9 @@ def test_dfnspec_local_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -588,10 +588,10 @@ def test_dfnspec_inherited_dims_includes_dis_dims(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -602,7 +602,7 @@ def test_dfnspec_inherited_dims_includes_dis_dims(): assert "nlay" in inherited assert "nrow" in inherited assert "ncol" in inherited - assert "nodes" in inherited # derived dim from gwf-dis, model-type scoped to "gwf" + assert "nodes" in inherited # derived dim from gwf-dis, model-scoped def test_dfnspec_inherited_dims_disv(): @@ -612,8 +612,8 @@ def test_dfnspec_inherited_dims_disv(): parent="gwf-nam", blocks={"dimensions": disv_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "ncpl": DimDef(field="ncpl", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "ncpl": DimDef(field="ncpl", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -632,8 +632,8 @@ def test_dfnspec_inherited_dims_disu(): parent="gwf-nam", blocks={"dimensions": disu_block}, dims={ - "nodes": DimDef(field="nodes", scope="gwf"), - "nja": DimDef(field="nja", scope="gwf"), + "nodes": DimDef(field="nodes", scope="model"), + "nja": DimDef(field="nja", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -653,16 +653,16 @@ def test_dfnspec_inherited_dims_excludes_own(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) chd = Package( name="gwf-chd", parent="gwf-nam", blocks={"dimensions": _dim_block("secret_dim")}, - dims={"secret_dim": DimDef(field="secret_dim", scope="gwf")}, + dims={"secret_dim": DimDef(field="secret_dim", scope="model")}, ) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) @@ -754,10 +754,10 @@ def _dis_spec() -> Dfns: parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) return Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -798,10 +798,10 @@ def test_dims_includes_derived(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -809,14 +809,14 @@ def test_dims_includes_derived(): assert "nodes" in known -def test_dims_includes_model_type_scoped(): - """A gwf-chd component inherits model-type-scoped dims from gwf-dis.""" +def test_dims_includes_model_scoped(): + """A gwf-chd component inherits model-scoped dims from gwf-dis.""" spec = _dis_spec() chd = _pkg("gwf-chd", parent="gwf-nam") spec2 = Dfns(components=dict(spec.components) | {"gwf-chd": chd}) known = spec2.dims("gwf-chd") - assert "nodes" in known # derived dim from gwf-dis, scope="gwf" - assert "nlay" in known # field-backed dim from gwf-dis, scope="gwf" + assert "nodes" in known # derived dim from gwf-dis, scope="model" + assert "nlay" in known # field-backed dim from gwf-dis, scope="model" # ============================================================================= @@ -844,13 +844,14 @@ def test_shape_element_valid_explicit_dim(): def test_shape_element_valid_inherited_dim(): - """A dim declared in a sibling component (model-type scoped) is valid.""" + """A dim declared in a sibling component (model-scoped) is valid.""" dis = Package( name="gwf-dis", + parent="gwf-nam", blocks=None, - dims={"nodes": DimDef(expr="42", scope="gwf")}, + dims={"nodes": DimDef(expr="42", scope="model")}, ) - test_pkg = Package(name="gwf-test", blocks=None) + test_pkg = Package(name="gwf-test", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-test": test_pkg}) known = spec.dims("gwf-test") @@ -1009,9 +1010,9 @@ def test_dfnspec_valid_top_level_array_shape(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1029,9 +1030,9 @@ def test_dfnspec_valid_array_in_record(): parent="gwf-nam", blocks={"dimensions": dis_block, "options": opt_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1069,9 +1070,9 @@ def test_dfnspec_invalid_array_shape_raises(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1088,10 +1089,10 @@ def test_dfnspec_array_shape_resolves_via_derived_dim(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1106,10 +1107,10 @@ def test_dfnspec_array_shape_resolves_via_sibling_dis(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="gwf"), - "nrow": DimDef(field="nrow", scope="gwf"), - "ncol": DimDef(field="ncol", scope="gwf"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="gwf"), + "nlay": DimDef(field="nlay", scope="model"), + "nrow": DimDef(field="nrow", scope="model"), + "ncol": DimDef(field="ncol", scope="model"), + "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), }, ) chd_arr = Array(name="head", dtype="double", shape=["nlay", "nodes"]) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 989dd4fe..649302b7 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -609,7 +609,7 @@ Row-level column lookups and bound annotations (` OMD: return OMD(items) +def _has_grid_dependent_shapes(dfn: Dfn) -> bool: + """Return True if any field uses a semicolon grid-type-dependent shape.""" + for block in dfn.get("blocks", {}).values(): + for field in block.values(): + if ";" in str(field.get("shape") or ""): + return True + return False + + def infer_parent(dfn: Dfn) -> str | None: """Infer a component's parent using naming conventions.""" if dfn["name"] == "sim-nam": return None if dfn["name"].endswith("-nam"): return "sim-nam" - if dfn["name"].startswith(("exg-", "sln-", "utl-")): + if dfn["name"].startswith(("exg-", "sln-")): + return "sim-nam" + if dfn["name"].startswith("utl-"): + # Grid-dependent shapes (semicolon notation) mean the utility must be + # model-attached, not simulation-level. + if _has_grid_dependent_shapes(dfn): + return "package" return "sim-nam" if "-" in dfn["name"]: mdl = dfn["name"].split("-")[0] @@ -768,8 +783,8 @@ def infer_parent(dfn: Dfn) -> str | None: def resolve_parent(dfn: Dfn) -> Dfn: """Infer and set a component's parent using naming conventions.""" - parent = infer_parent(dfn) - dfn["parent"] = parent + if dfn["parent"] is None: + dfn["parent"] = infer_parent(dfn) return dfn diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index 17ba2118..93a48dae 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -9,45 +9,58 @@ _IDENT_RE = re.compile(r"^[A-Za-z_]\w*$") _LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") -_SIM_PREFIXES: frozenset[str] = frozenset({"sim", "sln", "exg", "utl"}) -def _model_type_of(name: str) -> str | None: - """Extract model type from a component name, e.g. 'gwf' from 'gwf-npf'.""" - prefix = name.split("-")[0] if "-" in name else name - return None if prefix in _SIM_PREFIXES else prefix +def _scope_for( + parent: "str | list[str] | None", +) -> "Literal['component', 'model', 'simulation']": + """ + Derive the DimDef scope for dims in a component's dimensions block from its parent. + + - Parent is a model (``-nam``) or a generic type (``"model"``, + ``"package"``, ``"*"``) → ``"model"`` + - Parent is ``"sim-nam"`` (directly under simulation) → ``"simulation"`` + - Otherwise → ``"component"`` + """ + parents = ([parent] if isinstance(parent, str) else parent) if parent is not None else [] + for p in parents: + if p == "sim-nam": + return "simulation" + if p.endswith("-nam") or p in ("model", "package", "*"): + return "model" + return "component" -def _build_explicit_dims(dfn_name: str, blocks: dict[str, v2.Block]) -> dict[str, v2.DimDef]: +def _build_explicit_dims( + parent: "str | list[str] | None", + blocks: dict[str, v2.Block], +) -> dict[str, v2.DimDef]: """Build the dims section from a component's dimensions block.""" dims: dict[str, v2.DimDef] = {} dim_block = blocks.get("dimensions") if not dim_block: return dims - model_type = _model_type_of(dfn_name) - if dfn_name == "sim-tdis": - default_scope = "simulation" - elif model_type: - default_scope = model_type - else: - default_scope = "component" - + scope = _scope_for(parent) for fname, field in dim_block.fields.items(): if isinstance(field, v2.Integer): - dims[fname] = v2.DimDef(field=fname, scope=default_scope) + dims[fname] = v2.DimDef(field=fname, scope=scope) - # Add derived dims for standard MF6 grid discretization types. - if model_type: + if scope == "model": has = set(dims.keys()) if {"nlay", "nrow", "ncol"} <= has: - dims["nodes"] = v2.DimDef(expr="nlay * nrow * ncol", scope=model_type) - dims["ncelldim"] = v2.DimDef(expr="3", scope=model_type) + dims["ncpl"] = v2.DimDef(expr="nrow * ncol", scope="model") + dims["nodes"] = v2.DimDef(expr="nlay * nrow * ncol", scope="model") + dims["ncelldim"] = v2.DimDef(expr="3", scope="model") elif {"nlay", "ncpl"} <= has: - dims["nodes"] = v2.DimDef(expr="nlay * ncpl", scope=model_type) - dims["ncelldim"] = v2.DimDef(expr="2", scope=model_type) + dims["nodes"] = v2.DimDef(expr="nlay * ncpl", scope="model") + dims["ncelldim"] = v2.DimDef(expr="2", scope="model") + elif {"nrow", "ncol"} <= has: + dims["ncpl"] = v2.DimDef(expr="nrow * ncol", scope="model") + dims["nodes"] = v2.DimDef(expr="nrow * ncol", scope="model") + dims["ncelldim"] = v2.DimDef(expr="2", scope="model") elif "nodes" in has: - dims["ncelldim"] = v2.DimDef(expr="1", scope=model_type) + dims["ncelldim"] = v2.DimDef(expr="1", scope="model") return dims @@ -476,7 +489,7 @@ def _record_fields() -> dict: blocks, array_dims = _resolve_dimensions(blocks) blocks = _resolve_relations(blocks) - explicit_dims = _build_explicit_dims(name, blocks) + explicit_dims = _build_explicit_dims(dfn["parent"], blocks) dims = {**explicit_dims, **array_dims} or None d: dict[str, Any] = { diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 56801090..257f2468 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -263,7 +263,7 @@ class DimDef(BaseModel): field: str | None = None # name of the field that provides this dimension expr: str | None = None # derivation expression, e.g. "nlay * nrow * ncol" - scope: str # "component" | "model" | "simulation" | "" + scope: Literal["component", "model", "simulation"] = "component" @model_validator(mode="after") def _check_exclusive(self) -> "DimDef": @@ -276,13 +276,37 @@ def is_derived(self) -> bool: return self.expr is not None -_SIM_PREFIXES: frozenset[str] = frozenset({"sim", "sln", "exg", "utl"}) +def _parents_as_set(parent: "str | list[str] | None") -> set[str]: + if parent is None: + return set() + return {parent} if isinstance(parent, str) else set(parent) -def _model_type(name: str) -> str | None: - """Extract model type from a component name, e.g. 'gwf' from 'gwf-npf'.""" - prefix = name.split("-")[0] if "-" in name else name - return None if prefix in _SIM_PREFIXES else prefix +def _can_share_model( + req_parent: "str | list[str] | None", + dim_parent: "str | list[str] | None", +) -> bool: + """ + Return True if the requesting component (req_parent) can be in the same + model as the dim-defining component (dim_parent). + + The dim-provider's parent identifies which model it belongs to (a concrete + ``-nam`` name, e.g. ``"gwf-nam"``). The requesting component's parent + determines whether it can be in that model: an explicit match, or a generic + type like ``"model"`` or ``"package"`` meaning any model. + """ + dim_parents = _parents_as_set(dim_parent) + model_contexts = {p for p in dim_parents if p.endswith("-nam") and p != "sim-nam"} + if not model_contexts: + return False + + req_parents = _parents_as_set(req_parent) + for rp in req_parents: + if rp in ("model", "package", "*"): + return True + if rp in model_contexts: + return True + return False def _resolve_derived_dims(component: "ComponentBase", known_dims: set[str]) -> list[str]: @@ -668,9 +692,19 @@ def local_dims(self, component_name: str) -> set[str]: return set((self.components[component_name].dims or {}).keys()) def inherited_dims(self, component_name: str) -> set[str]: - """Dim names visible to ``component_name`` from other components.""" + """ + Dim names visible to ``component_name`` from other components. + + - ``"simulation"`` scope: always visible. + - ``"model"`` scope: visible when the requesting component can share a + model with the dim-defining component, determined purely from parent + attributes (no hardcoded model-type strings). + - ``"component"`` scope: visible when the dim-defining component is + explicitly listed as a parent of the requesting component (subpackage). + """ inherited: set[str] = set() component = self.components[component_name] + req_parent = component.parent for cname, c in self.components.items(): if cname == component_name: continue @@ -679,10 +713,10 @@ def inherited_dims(self, component_name: str) -> set[str]: case "simulation": inherited.add(dim_name) case "model": - if "model" in component.parent or cname in component.parent: + if _can_share_model(req_parent, c.parent): inherited.add(dim_name) case "component": - if cname in component.parent: + if cname in _parents_as_set(req_parent): inherited.add(dim_name) return inherited From 6d0ec17107f2690eadb1fdfc8c01d6968eff00fc Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Thu, 21 May 2026 17:37:00 -0700 Subject: [PATCH 15/29] update spec --- docs/md/dfnspec.md | 118 ++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 649302b7..274677ae 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -45,7 +45,6 @@ - [Integer](#integer) - [Type-specific attributes](#type-specific-attributes-3) - [`valid`](#valid-1) - - [`dimension`](#dimension) - [`time_series`](#time_series) - [`pk`](#pk-1) - [`fk`](#fk-1) @@ -75,7 +74,9 @@ - [Array dimensions](#array-dimensions) - [Derived dimensions](#derived-dimensions) - [Row-level column lookups](#row-level-column-lookups) + - [Intra-record sibling references](#intra-record-sibling-references) - [Scope and resolution](#scope-and-resolution) + - [Dimension scope](#dimension-scope) - [Primary/foreign keys](#primaryforeign-keys) - [Examples](#examples) @@ -94,7 +95,7 @@ Component definitions consist primarily of a name, zero or more block definition - `blocks`: block definitions - `parent`: parent component(s) - `schema_version`: DFN schema version -- `derived_dims`: dimensions computed from other dimensions +- `dims`: named dimensions (field-backed or derived) available for use in array shapes Components may refer to, i.e. be constrained by, other components. Cross-component constraints include parent-child relations, solution compatibility, and format variants. @@ -334,10 +335,6 @@ Type `integer`. `[integer] | null`. Permitted values (enumeration constraint). Empty list is treated as absent. -###### `dimension` - -`"record" | "component" | "model" | "simulation" | true | false | null (default: null)`. Declares this field as a dimension source and specifies its scope. See [Scope and resolution](#scope-and-resolution). A `true` value indicates `"component"` scope; `false` and `null` are equivalent. - ###### `time_series` `boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. @@ -416,12 +413,6 @@ Each declared shape expression is either a global dimension name (explicit or de `string | null (default: null)`. Names the field (within the same component) whose runtime length determines how many times this field is read sequentially within an array block, with each reading appended to an accumulated sequence. See `repeat` section below. -###### `dimension` - -`"component" | "model" | "simulation" | null (default: null)`. Marks this self-sizing array as a dimension source at the given scope: the array's name may appear in other arrays' `shape` expressions to mean "one value per element of this array." Valid only on self-sizing arrays (`shape` must be empty); a schema error on arrays with a declared shape. - -Because a self-sizing array is read inline and dynamically sized, the parser knows its element count immediately after reading the line — no separate integer field is needed to declare the size in advance. That count is what other arrays reference when they name this array in their `shape`. The dtype of the dimension-source array is not constrained: a string array whose elements are named identifiers (e.g. auxiliary variable names) and a numeric array whose elements happen to fix a count both provide the same thing to the shape system — a dynamic integer size. `"record"` scope is not valid for arrays. - #### Record Type `record`. Product type. In MF6 input files, records appear on a single line. Record subfields may or may not be `tagged`. While blocks can be considered product types also, in the DFN specification only records are considered fields; blocks are considered named collections of related fields. @@ -458,15 +449,33 @@ Type `list`. Collection type. Unlimited but for one rule: a list may not contain ### Array dimensions -A field defined in one component can be referenced by name in the `shape` expression of an array field in the same or another component. Two field types may serve as dimension sources: +Dimensions are declared at the component level via the `dims` map, not on individual fields. Each entry in `dims` is a `DimDef`: + +```yaml +dims: + nlay: + field: nlay # backed by an integer field named 'nlay' in this component + scope: model + nodes: + expr: "nlay * nrow * ncol" # derived from other dims + scope: model + nper: + field: nper + scope: simulation +``` + +A `DimDef` has exactly one of: +- `field`: the name of an `integer` field in this component that provides the dimension value +- `expr`: a Python arithmetic expression that derives the dimension from other known dims -- `integer` fields with `dimension: true` -- `array` fields with `dtype: "string"` and `dimension: true` — the array's name becomes a valid shape dim meaning "one value per element of this string array" +And a `scope` (see [Scope and resolution](#scope-and-resolution)) that controls which other components can see it. + +Self-sizing `array` fields (those with `shape: []`) may also serve as dimension sources: any such array's name may appear in a `shape` expression to mean "one element per item in this array." These are registered in `dims` with `field` pointing to the array name. Shape expressions for non-string arrays may use one of three structural forms. All three may additionally carry a bound annotation: - **Dim reference** (`^[A-Za-z_]\w*$`): a plain identifier resolved via the scope chain (explicit → derived → inherited dims). When the array is a subfield of a record and the identifier does not resolve globally, resolution falls back to intra-record sibling scope (see below). -- **Intra-record sibling reference**: a dim reference that names a sibling `integer` or `dimension: true` `array` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. +- **Intra-record sibling reference**: a dim reference that names a sibling `integer` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. - **Row-level column lookup** (`block.column(fk_field)`): a cross-list per-row quantity, valid only for array subfields of records. See below. Any dim reference (either of the first two forms) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`). The dim portion validates normally; the bound is advisory and is not enforced by the MF6 parser. @@ -475,29 +484,37 @@ A shape expression that does not match one of these forms is a schema validation #### Derived dimensions -The optional component attribute `derived_dims` maps dimension names to expressions with which to evaluate the dimension size. Expressions use Python arithmetic syntax. Operands may be: +A `DimDef` with an `expr` rather than a `field` is a derived dimension. Its expression uses Python arithmetic syntax. Operands may be: -- Explicit dimensions: any `dimension: true` field in this component -- Derived dimensions: another derived dimension; circular dependencies are a schema error +- Explicit dimensions: any field-backed dim in this component's `dims` +- Other derived dims: another derived dim in this component; circular dependencies are a schema error - Functions of columns in a tabular list block: `sum(block.list.column)` sums the integer values of `column` across all rows of list field `list` in block `block`. When the list field shares its name with its containing block — the MF6 convention — the block qualifier may be omitted: `sum(list.column)`. +Derived dims carry the same `scope` as field-backed dims and are visible to other components under the same rules. + Canonical examples: ```yaml -# gwf-dis: nodes is derived; ncpl is also derived to give packages a uniform -# per-layer cell count dim regardless of discretization type (DISV has ncpl -# as an explicit dim; DIS derives it from nrow * ncol) -nodes: "nlay * nrow * ncol" -ncpl: "nrow * ncol" - -# gwf-disv: ncpl is explicit; nodes is derived -nodes: "nlay * ncpl" - -# gwf-lak -total_lake_connections: "sum(packagedata.nlakeconn)" - -# gwf-evt -nseg_minus_1: "nseg - 1" +dims: + # gwf-dis: nodes and ncpl are derived to give packages a uniform dim + # regardless of discretization type (DISV has ncpl explicit; DIS derives it) + nlay: {field: nlay, scope: model} + nrow: {field: nrow, scope: model} + ncol: {field: ncol, scope: model} + ncpl: {expr: "nrow * ncol", scope: model} + nodes: {expr: "nlay * nrow * ncol", scope: model} + ncelldim: {expr: "3", scope: model} + + # gwf-disv: ncpl is explicit; nodes is derived + ncpl: {field: ncpl, scope: model} + nodes: {expr: "nlay * ncpl", scope: model} + ncelldim: {expr: "2", scope: model} + + # gwf-lak + total_lake_connections: {expr: "sum(packagedata.nlakeconn)", scope: component} + + # sim-tdis + nper: {field: nper, scope: simulation} ``` #### Row-level column lookups @@ -557,7 +574,7 @@ connectiondata: #### Intra-record sibling references -A record may also be a variadic tuple without any FK-linked list. If a record contains an `integer` field (or a string-dim `array`) followed by an `array` whose shape names that preceding field, the record is self-describing: it carries its own count. The `array`'s shape element is a plain identifier that resolves to the sibling field, not to a global dim. +A record may also be a variadic tuple without any FK-linked list. If a record contains an `integer` field followed by an `array` whose shape names that field, the record is self-describing: it carries its own count. The `array`'s shape element is a plain identifier that resolves to the sibling field, not to a global dim. ```yaml connectiondata: @@ -580,7 +597,7 @@ connectiondata: Validation rules: - Valid only when the array is a subfield of a record (not a top-level block field). -- The sibling field must be an `integer` with `dimension: "record"`. The `"record"` scope annotation makes the role explicit and prevents accidental resolution of unrelated integer fields. +- The sibling field must be an `integer`. No special annotation is required on the sibling. - Resolution order: the scope chain is tried first; sibling resolution is only the fallback when the identifier does not resolve globally. #### Bound-annotated shape expressions @@ -598,28 +615,29 @@ sfacval: Dim references (plain identifiers) resolve in this order: -1. Local explicit dims: `integer` fields with `dimension` set and self-sizing `array` fields (i.e. `shape: []`) with `dimension` set, in this component -2. Local derived dims: entries in this component's `derived_dims`, resolved in dependency order -3. Inherited dims: explicit dims from other components in the spec (filtered by scope — `"model"` dims available to packages in the same model, `"simulation"` dims available to all) -4. Intra-record siblings with `dimension: "record"`: a sibling `integer` in the same enclosing record that has been explicitly marked as a per-row inline count — fallback when steps 1–3 all fail and the array is inside a record. +1. **Local dims**: entries in this component's `dims` map — both field-backed and derived — resolved in dependency order. +2. **Inherited dims**: dims from other components in the spec, filtered by their `scope` and the requesting component's `parent` attribute (see below). +3. **Intra-record sibling**: a sibling `integer` in the same enclosing record — fallback when steps 1–2 fail and the array is inside a record. Row-level column lookups and bound annotations (` Date: Thu, 21 May 2026 17:51:10 -0700 Subject: [PATCH 16/29] fixes --- docs/md/dfnspec.md | 24 +++++------ modflow_devtools/dfns/schema.py | 76 +++++---------------------------- 2 files changed, 22 insertions(+), 78 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 274677ae..e03a3f78 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -39,6 +39,7 @@ - [Type-specific attributes](#type-specific-attributes-2) - [`valid`](#valid) - [`case_sensitive`](#case_sensitive) + - [`time_series`](#time_series) - [`pk`](#pk) - [`fk`](#fk) - [`fk_ref`](#fk_ref) @@ -52,7 +53,7 @@ - [Double](#double) - [Type-specific attributes](#type-specific-attributes-4) - [`time_series`](#time_series-1) - - [Path](#path) + - [File](#file) - [Type-specific attributes](#type-specific-attributes-5) - [`mode`](#mode) - [Composites](#composites) @@ -309,6 +310,10 @@ Type `string`. `boolean (default: false)`. Indicates that the string's case must be preserved. The MF6 parser uppercases strings by default. +###### `time_series` + +`boolean (default: false)`. Marks fields where the parser accepts either a string literal or a time-series name (referencing a `utl-ts` object). + ###### `pk` `boolean (default: false)`. Marks this scalar as the primary key of its containing list's item record. Valid only on integer or string scalars that are columns in a list item record. Exactly one column per list item may be marked pk. @@ -327,10 +332,6 @@ Type `integer`. ##### Type-specific attributes -###### `tagged` - -`boolean (default: true)`. Indicates that the field value should be preceded by the field name. - ###### `valid` `[integer] | null`. Permitted values (enumeration constraint). Empty list is treated as absent. @@ -357,17 +358,13 @@ Type `double`. ##### Type-specific attributes -###### `tagged` - -`boolean (default: true)`. Indicates that the field value should be preceded by the field name. - ###### `time_series` `boolean (default: false)`. Marks fields where the parser accepts either a numeric literal or a time-series name (referencing a `utl-ts` object). Not inferable from structural type. Also appears on array fields (where it references a `utl-tas` object instead). Note that `utl-tas` currently only works with layered arrays, not full-grid arrays, though generalizing has been considered. -#### Path +#### File -Type `path`. +Type `file`. ##### Type-specific attributes @@ -472,13 +469,14 @@ And a `scope` (see [Scope and resolution](#scope-and-resolution)) that controls Self-sizing `array` fields (those with `shape: []`) may also serve as dimension sources: any such array's name may appear in a `shape` expression to mean "one element per item in this array." These are registered in `dims` with `field` pointing to the array name. -Shape expressions for non-string arrays may use one of three structural forms. All three may additionally carry a bound annotation: +Shape expressions for non-string arrays may use one of four structural forms. Dim references may additionally carry a bound annotation: - **Dim reference** (`^[A-Za-z_]\w*$`): a plain identifier resolved via the scope chain (explicit → derived → inherited dims). When the array is a subfield of a record and the identifier does not resolve globally, resolution falls back to intra-record sibling scope (see below). - **Intra-record sibling reference**: a dim reference that names a sibling `integer` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. +- **Arithmetic offset** (`dim [+-] integer`): a dim reference with an integer offset, e.g. `nlay + 1`. Only the dim portion is validated; the offset is accepted as-is. - **Row-level column lookup** (`block.column(fk_field)`): a cross-list per-row quantity, valid only for array subfields of records. See below. -Any dim reference (either of the first two forms) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`). The dim portion validates normally; the bound is advisory and is not enforced by the MF6 parser. +Any dim reference (including the dim portion of an arithmetic offset) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`). The dim portion validates normally; the bound is advisory and is not enforced by the MF6 parser. A shape expression that does not match one of these forms is a schema validation error. String arrays (`dtype: "string"`) must have empty `shape`. diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 257f2468..eb37552f 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -17,6 +17,15 @@ class FieldBase(BaseModel): + name: str + longname: str | None = None + description: str | None = None + optional: bool = False + default: Any | None = None + developmode: bool = False + netcdf: bool = False + tagged: bool = True + @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": type_name = d.get("type") @@ -25,7 +34,7 @@ def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": "string": String, "integer": Integer, "double": Double, - "path": File, + "file": File, "array": Array, "record": Record, "union": Union, @@ -43,25 +52,10 @@ def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": class Keyword(FieldBase): type: Literal["keyword"] = PydanticField(default="keyword", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False class String(FieldBase): type: Literal["string"] = PydanticField(default="string", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False - tagged: bool = True valid: list[str] | None = None case_sensitive: bool = False time_series: bool = False @@ -72,14 +66,6 @@ class String(FieldBase): class Integer(FieldBase): type: Literal["integer"] = PydanticField(default="integer", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False - tagged: bool = True valid: list[int] | None = None time_series: bool = False pk: bool = False @@ -89,25 +75,11 @@ class Integer(FieldBase): class Double(FieldBase): type: Literal["double"] = PydanticField(default="double", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False - tagged: bool = True time_series: bool = False class File(FieldBase): type: Literal["file"] = PydanticField(default="file", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False mode: Literal["filein", "fileout"] @@ -119,13 +91,6 @@ class File(FieldBase): class Array(FieldBase): type: Literal["array"] = PydanticField(default="array", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False dtype: Literal["keyword", "integer", "double", "string"] shape: list[str] = [] time_series: bool = False @@ -134,12 +99,6 @@ class Array(FieldBase): class Record(FieldBase): type: Literal["record"] = PydanticField(default="record", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False fields: "dict[str, Scalar | Array | Record | Union]" = PydanticField(default_factory=dict) @property @@ -149,12 +108,6 @@ def children(self) -> "dict[str, Field]": class Union(FieldBase): type: Literal["union"] = PydanticField(default="union", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False arms: "dict[str, Scalar | Array | Record]" = PydanticField(default_factory=dict) @property @@ -164,13 +117,6 @@ def children(self) -> "dict[str, Field]": class List(FieldBase): type: Literal["list"] = PydanticField(default="list", frozen=True) - name: str - longname: str | None = None - description: str | None = None - optional: bool = False - default: Any | None = None - developmode: bool = False - netcdf: bool = False item: "Record | Union" @property @@ -397,7 +343,7 @@ class Simulation(ComponentBase): class Model(ComponentBase): type: Literal["model"] = "model" - solution: str | list[str] | None = None # compatible solution type(s) + solution: Literal["ims", "ems", "sln-ims", "sln-ems"] | None = None class Package(ComponentBase): From c73d3e7b1d513226bc2834212153552f4b699c1b Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 05:26:59 -0700 Subject: [PATCH 17/29] cleanup --- autotest/autotest/temp/dfn/chf-cdb.dfn | 183 ---- autotest/autotest/temp/dfn/chf-chd.dfn | 208 ---- autotest/autotest/temp/dfn/chf-cxs.dfn | 100 -- autotest/autotest/temp/dfn/chf-dfw.dfn | 143 --- autotest/autotest/temp/dfn/chf-disv1d.dfn | 250 ----- autotest/autotest/temp/dfn/chf-evp.dfn | 211 ---- autotest/autotest/temp/dfn/chf-flw.dfn | 207 ---- autotest/autotest/temp/dfn/chf-ic.dfn | 22 - autotest/autotest/temp/dfn/chf-nam.dfn | 99 -- autotest/autotest/temp/dfn/chf-oc.dfn | 350 ------- autotest/autotest/temp/dfn/chf-pcp.dfn | 211 ---- autotest/autotest/temp/dfn/chf-sto.dfn | 60 -- autotest/autotest/temp/dfn/chf-zdg.dfn | 237 ----- autotest/autotest/temp/dfn/common.dfn | 61 -- autotest/autotest/temp/dfn/exg-chfgwf.dfn | 136 --- autotest/autotest/temp/dfn/exg-gwegwe.dfn | 281 ----- autotest/autotest/temp/dfn/exg-gwfgwe.dfn | 3 - autotest/autotest/temp/dfn/exg-gwfgwf.dfn | 321 ------ autotest/autotest/temp/dfn/exg-gwfgwt.dfn | 3 - autotest/autotest/temp/dfn/exg-gwfprt.dfn | 3 - autotest/autotest/temp/dfn/exg-gwtgwt.dfn | 281 ----- autotest/autotest/temp/dfn/exg-olfgwf.dfn | 136 --- autotest/autotest/temp/dfn/gwe-adv.dfn | 19 - autotest/autotest/temp/dfn/gwe-cnd.dfn | 118 --- autotest/autotest/temp/dfn/gwe-ctp.dfn | 213 ---- autotest/autotest/temp/dfn/gwe-dis.dfn | 239 ----- autotest/autotest/temp/dfn/gwe-disu.dfn | 337 ------ autotest/autotest/temp/dfn/gwe-disv.dfn | 320 ------ autotest/autotest/temp/dfn/gwe-esl.dfn | 211 ---- autotest/autotest/temp/dfn/gwe-est.dfn | 108 -- autotest/autotest/temp/dfn/gwe-fmi.dfn | 59 -- autotest/autotest/temp/dfn/gwe-ic.dfn | 33 - autotest/autotest/temp/dfn/gwe-lke.dfn | 481 --------- autotest/autotest/temp/dfn/gwe-mve.dfn | 106 -- autotest/autotest/temp/dfn/gwe-mwe.dfn | 447 -------- autotest/autotest/temp/dfn/gwe-nam.dfn | 210 ---- autotest/autotest/temp/dfn/gwe-oc.dfn | 318 ------ autotest/autotest/temp/dfn/gwe-sfe.dfn | 480 --------- autotest/autotest/temp/dfn/gwe-ssm.dfn | 125 --- autotest/autotest/temp/dfn/gwe-uze.dfn | 438 -------- autotest/autotest/temp/dfn/gwf-api.dfn | 100 -- autotest/autotest/temp/dfn/gwf-buy.dfn | 148 --- autotest/autotest/temp/dfn/gwf-chd.dfn | 222 ---- autotest/autotest/temp/dfn/gwf-chdg.dfn | 170 --- autotest/autotest/temp/dfn/gwf-csub.dfn | 793 -------------- autotest/autotest/temp/dfn/gwf-dis.dfn | 237 ----- autotest/autotest/temp/dfn/gwf-disu.dfn | 337 ------ autotest/autotest/temp/dfn/gwf-disv.dfn | 320 ------ autotest/autotest/temp/dfn/gwf-drn.dfn | 251 ----- autotest/autotest/temp/dfn/gwf-drng.dfn | 198 ---- autotest/autotest/temp/dfn/gwf-evt.dfn | 295 ------ autotest/autotest/temp/dfn/gwf-evta.dfn | 220 ---- autotest/autotest/temp/dfn/gwf-ghb.dfn | 232 ----- autotest/autotest/temp/dfn/gwf-ghbg.dfn | 179 ---- autotest/autotest/temp/dfn/gwf-gnc.dfn | 100 -- autotest/autotest/temp/dfn/gwf-hfb.dfn | 77 -- autotest/autotest/temp/dfn/gwf-ic.dfn | 33 - autotest/autotest/temp/dfn/gwf-lak.dfn | 884 ---------------- autotest/autotest/temp/dfn/gwf-maw.dfn | 762 -------------- autotest/autotest/temp/dfn/gwf-mvr.dfn | 265 ----- autotest/autotest/temp/dfn/gwf-nam.dfn | 226 ---- autotest/autotest/temp/dfn/gwf-npf.dfn | 375 ------- autotest/autotest/temp/dfn/gwf-oc.dfn | 317 ------ autotest/autotest/temp/dfn/gwf-rch.dfn | 221 ---- autotest/autotest/temp/dfn/gwf-rcha.dfn | 201 ---- autotest/autotest/temp/dfn/gwf-riv.dfn | 243 ----- autotest/autotest/temp/dfn/gwf-rivg.dfn | 191 ---- autotest/autotest/temp/dfn/gwf-sfr.dfn | 970 ------------------ autotest/autotest/temp/dfn/gwf-sto.dfn | 187 ---- autotest/autotest/temp/dfn/gwf-uzf.dfn | 611 ----------- autotest/autotest/temp/dfn/gwf-vsc.dfn | 177 ---- autotest/autotest/temp/dfn/gwf-wel.dfn | 285 ----- autotest/autotest/temp/dfn/gwf-welg.dfn | 233 ----- autotest/autotest/temp/dfn/gwt-adv.dfn | 18 - autotest/autotest/temp/dfn/gwt-api.dfn | 100 -- autotest/autotest/temp/dfn/gwt-cnc.dfn | 213 ---- autotest/autotest/temp/dfn/gwt-dis.dfn | 239 ----- autotest/autotest/temp/dfn/gwt-disu.dfn | 337 ------ autotest/autotest/temp/dfn/gwt-disv.dfn | 320 ------ autotest/autotest/temp/dfn/gwt-dsp.dfn | 106 -- autotest/autotest/temp/dfn/gwt-fmi.dfn | 59 -- autotest/autotest/temp/dfn/gwt-ic.dfn | 33 - autotest/autotest/temp/dfn/gwt-ist.dfn | 377 ------- autotest/autotest/temp/dfn/gwt-lkt.dfn | 460 --------- autotest/autotest/temp/dfn/gwt-mst.dfn | 168 --- autotest/autotest/temp/dfn/gwt-mvt.dfn | 106 -- autotest/autotest/temp/dfn/gwt-mwt.dfn | 427 -------- autotest/autotest/temp/dfn/gwt-nam.dfn | 210 ---- autotest/autotest/temp/dfn/gwt-oc.dfn | 318 ------ autotest/autotest/temp/dfn/gwt-sft.dfn | 460 --------- autotest/autotest/temp/dfn/gwt-src.dfn | 222 ---- autotest/autotest/temp/dfn/gwt-ssm.dfn | 125 --- autotest/autotest/temp/dfn/gwt-uzt.dfn | 438 -------- autotest/autotest/temp/dfn/olf-cdb.dfn | 183 ---- autotest/autotest/temp/dfn/olf-chd.dfn | 208 ---- autotest/autotest/temp/dfn/olf-cxs.dfn | 100 -- autotest/autotest/temp/dfn/olf-dfw.dfn | 143 --- autotest/autotest/temp/dfn/olf-dis2d.dfn | 163 --- autotest/autotest/temp/dfn/olf-disv1d.dfn | 261 ----- autotest/autotest/temp/dfn/olf-disv2d.dfn | 247 ----- autotest/autotest/temp/dfn/olf-evp.dfn | 211 ---- autotest/autotest/temp/dfn/olf-flw.dfn | 207 ---- autotest/autotest/temp/dfn/olf-ic.dfn | 22 - autotest/autotest/temp/dfn/olf-nam.dfn | 99 -- autotest/autotest/temp/dfn/olf-oc.dfn | 350 ------- autotest/autotest/temp/dfn/olf-pcp.dfn | 211 ---- autotest/autotest/temp/dfn/olf-sto.dfn | 60 -- autotest/autotest/temp/dfn/olf-zdg.dfn | 237 ----- autotest/autotest/temp/dfn/prt-dis.dfn | 230 ----- autotest/autotest/temp/dfn/prt-disv.dfn | 313 ------ autotest/autotest/temp/dfn/prt-fmi.dfn | 50 - autotest/autotest/temp/dfn/prt-mip.dfn | 41 - autotest/autotest/temp/dfn/prt-nam.dfn | 73 -- autotest/autotest/temp/dfn/prt-oc.dfn | 451 -------- autotest/autotest/temp/dfn/prt-prp.dfn | 508 --------- autotest/autotest/temp/dfn/sim-nam.dfn | 255 ----- autotest/autotest/temp/dfn/sim-tdis.dfn | 113 -- autotest/autotest/temp/dfn/sln-ems.dfn | 1 - autotest/autotest/temp/dfn/sln-ims.dfn | 389 ------- autotest/autotest/temp/dfn/sln-pts.dfn | 178 ---- autotest/autotest/temp/dfn/swf-cdb.dfn | 183 ---- autotest/autotest/temp/dfn/swf-chd.dfn | 208 ---- autotest/autotest/temp/dfn/swf-cxs.dfn | 100 -- autotest/autotest/temp/dfn/swf-dfw.dfn | 143 --- autotest/autotest/temp/dfn/swf-dis2d.dfn | 163 --- autotest/autotest/temp/dfn/swf-disv1d.dfn | 250 ----- autotest/autotest/temp/dfn/swf-disv2d.dfn | 247 ----- autotest/autotest/temp/dfn/swf-evp.dfn | 211 ---- autotest/autotest/temp/dfn/swf-flw.dfn | 207 ---- autotest/autotest/temp/dfn/swf-ic.dfn | 22 - autotest/autotest/temp/dfn/swf-nam.dfn | 99 -- autotest/autotest/temp/dfn/swf-oc.dfn | 350 ------- autotest/autotest/temp/dfn/swf-pcp.dfn | 211 ---- autotest/autotest/temp/dfn/swf-sto.dfn | 60 -- autotest/autotest/temp/dfn/swf-zdg.dfn | 237 ----- .../autotest/temp/dfn/toml-v1_1/sim-nam.toml | 588 ----------- autotest/autotest/temp/dfn/toml/chf-cdb.toml | 124 --- autotest/autotest/temp/dfn/toml/chf-chd.toml | 146 --- autotest/autotest/temp/dfn/toml/chf-cxs.toml | 82 -- autotest/autotest/temp/dfn/toml/chf-dfw.toml | 98 -- .../autotest/temp/dfn/toml/chf-disv1d.toml | 198 ---- autotest/autotest/temp/dfn/toml/chf-evp.toml | 146 --- autotest/autotest/temp/dfn/toml/chf-flw.toml | 146 --- autotest/autotest/temp/dfn/toml/chf-ic.toml | 23 - autotest/autotest/temp/dfn/toml/chf-nam.toml | 78 -- autotest/autotest/temp/dfn/toml/chf-oc.toml | 187 ---- autotest/autotest/temp/dfn/toml/chf-pcp.toml | 146 --- autotest/autotest/temp/dfn/toml/chf-sto.toml | 33 - autotest/autotest/temp/dfn/toml/chf-zdg.toml | 159 --- .../autotest/temp/dfn/toml/exg-chfgwf.toml | 93 -- .../autotest/temp/dfn/toml/exg-gwegwe.toml | 218 ---- .../autotest/temp/dfn/toml/exg-gwfgwe.toml | 3 - .../autotest/temp/dfn/toml/exg-gwfgwf.toml | 252 ----- .../autotest/temp/dfn/toml/exg-gwfgwt.toml | 3 - .../autotest/temp/dfn/toml/exg-gwfprt.toml | 3 - .../autotest/temp/dfn/toml/exg-gwtgwt.toml | 218 ---- .../autotest/temp/dfn/toml/exg-olfgwf.toml | 93 -- autotest/autotest/temp/dfn/toml/gwe-adv.toml | 41 - autotest/autotest/temp/dfn/toml/gwe-cnd.toml | 109 -- autotest/autotest/temp/dfn/toml/gwe-ctp.toml | 146 --- autotest/autotest/temp/dfn/toml/gwe-dis.toml | 201 ---- autotest/autotest/temp/dfn/toml/gwe-disu.toml | 288 ------ autotest/autotest/temp/dfn/toml/gwe-disv.toml | 249 ----- autotest/autotest/temp/dfn/toml/gwe-esl.toml | 146 --- autotest/autotest/temp/dfn/toml/gwe-est.toml | 94 -- autotest/autotest/temp/dfn/toml/gwe-fmi.toml | 46 - autotest/autotest/temp/dfn/toml/gwe-ic.toml | 30 - autotest/autotest/temp/dfn/toml/gwe-lke.toml | 290 ------ autotest/autotest/temp/dfn/toml/gwe-mve.toml | 69 -- autotest/autotest/temp/dfn/toml/gwe-mwe.toml | 272 ----- autotest/autotest/temp/dfn/toml/gwe-nam.toml | 133 --- autotest/autotest/temp/dfn/toml/gwe-oc.toml | 166 --- autotest/autotest/temp/dfn/toml/gwe-sfe.toml | 290 ------ autotest/autotest/temp/dfn/toml/gwe-ssm.toml | 84 -- autotest/autotest/temp/dfn/toml/gwe-uze.toml | 266 ----- autotest/autotest/temp/dfn/toml/gwf-api.toml | 68 -- autotest/autotest/temp/dfn/toml/gwf-buy.toml | 97 -- autotest/autotest/temp/dfn/toml/gwf-chd.toml | 152 --- autotest/autotest/temp/dfn/toml/gwf-chdg.toml | 116 --- autotest/autotest/temp/dfn/toml/gwf-csub.toml | 509 --------- autotest/autotest/temp/dfn/toml/gwf-dis.toml | 201 ---- autotest/autotest/temp/dfn/toml/gwf-disu.toml | 290 ------ autotest/autotest/temp/dfn/toml/gwf-disv.toml | 249 ----- autotest/autotest/temp/dfn/toml/gwf-drn.toml | 171 --- autotest/autotest/temp/dfn/toml/gwf-drng.toml | 138 --- autotest/autotest/temp/dfn/toml/gwf-evt.toml | 212 ---- autotest/autotest/temp/dfn/toml/gwf-evta.toml | 154 --- autotest/autotest/temp/dfn/toml/gwf-ghb.toml | 158 --- autotest/autotest/temp/dfn/toml/gwf-ghbg.toml | 125 --- autotest/autotest/temp/dfn/toml/gwf-gnc.toml | 89 -- autotest/autotest/temp/dfn/toml/gwf-hfb.toml | 58 -- autotest/autotest/temp/dfn/toml/gwf-ic.toml | 30 - autotest/autotest/temp/dfn/toml/gwf-lak.toml | 558 ---------- autotest/autotest/temp/dfn/toml/gwf-maw.toml | 465 --------- autotest/autotest/temp/dfn/toml/gwf-mvr.toml | 170 --- autotest/autotest/temp/dfn/toml/gwf-nam.toml | 144 --- autotest/autotest/temp/dfn/toml/gwf-npf.toml | 291 ------ autotest/autotest/temp/dfn/toml/gwf-oc.toml | 166 --- autotest/autotest/temp/dfn/toml/gwf-rch.toml | 152 --- autotest/autotest/temp/dfn/toml/gwf-rcha.toml | 135 --- autotest/autotest/temp/dfn/toml/gwf-riv.toml | 165 --- autotest/autotest/temp/dfn/toml/gwf-rivg.toml | 135 --- autotest/autotest/temp/dfn/toml/gwf-sfr.toml | 599 ----------- autotest/autotest/temp/dfn/toml/gwf-sto.toml | 117 --- autotest/autotest/temp/dfn/toml/gwf-uzf.toml | 400 -------- autotest/autotest/temp/dfn/toml/gwf-vsc.toml | 138 --- autotest/autotest/temp/dfn/toml/gwf-wel.toml | 185 ---- autotest/autotest/temp/dfn/toml/gwf-welg.toml | 149 --- autotest/autotest/temp/dfn/toml/gwt-adv.toml | 46 - autotest/autotest/temp/dfn/toml/gwt-api.toml | 68 -- autotest/autotest/temp/dfn/toml/gwt-cnc.toml | 146 --- autotest/autotest/temp/dfn/toml/gwt-dis.toml | 201 ---- autotest/autotest/temp/dfn/toml/gwt-disu.toml | 290 ------ autotest/autotest/temp/dfn/toml/gwt-disv.toml | 249 ----- autotest/autotest/temp/dfn/toml/gwt-dsp.toml | 98 -- autotest/autotest/temp/dfn/toml/gwt-fmi.toml | 46 - autotest/autotest/temp/dfn/toml/gwt-ic.toml | 30 - autotest/autotest/temp/dfn/toml/gwt-ist.toml | 268 ----- autotest/autotest/temp/dfn/toml/gwt-lkt.toml | 278 ----- autotest/autotest/temp/dfn/toml/gwt-mst.toml | 159 --- autotest/autotest/temp/dfn/toml/gwt-mvt.toml | 69 -- autotest/autotest/temp/dfn/toml/gwt-mwt.toml | 260 ----- autotest/autotest/temp/dfn/toml/gwt-nam.toml | 133 --- autotest/autotest/temp/dfn/toml/gwt-oc.toml | 166 --- autotest/autotest/temp/dfn/toml/gwt-sft.toml | 278 ----- autotest/autotest/temp/dfn/toml/gwt-src.toml | 152 --- autotest/autotest/temp/dfn/toml/gwt-ssm.toml | 84 -- autotest/autotest/temp/dfn/toml/gwt-uzt.toml | 266 ----- autotest/autotest/temp/dfn/toml/olf-cdb.toml | 124 --- autotest/autotest/temp/dfn/toml/olf-chd.toml | 146 --- autotest/autotest/temp/dfn/toml/olf-cxs.toml | 82 -- autotest/autotest/temp/dfn/toml/olf-dfw.toml | 98 -- .../autotest/temp/dfn/toml/olf-dis2d.toml | 139 --- .../autotest/temp/dfn/toml/olf-disv1d.toml | 207 ---- .../autotest/temp/dfn/toml/olf-disv2d.toml | 194 ---- autotest/autotest/temp/dfn/toml/olf-evp.toml | 146 --- autotest/autotest/temp/dfn/toml/olf-flw.toml | 146 --- autotest/autotest/temp/dfn/toml/olf-ic.toml | 23 - autotest/autotest/temp/dfn/toml/olf-nam.toml | 78 -- autotest/autotest/temp/dfn/toml/olf-oc.toml | 187 ---- autotest/autotest/temp/dfn/toml/olf-pcp.toml | 146 --- autotest/autotest/temp/dfn/toml/olf-sto.toml | 33 - autotest/autotest/temp/dfn/toml/olf-zdg.toml | 159 --- autotest/autotest/temp/dfn/toml/prt-dis.toml | 196 ---- autotest/autotest/temp/dfn/toml/prt-disv.toml | 246 ----- autotest/autotest/temp/dfn/toml/prt-fmi.toml | 40 - autotest/autotest/temp/dfn/toml/prt-mip.toml | 43 - autotest/autotest/temp/dfn/toml/prt-nam.toml | 60 -- autotest/autotest/temp/dfn/toml/prt-oc.toml | 286 ------ autotest/autotest/temp/dfn/toml/prt-prp.toml | 341 ------ autotest/autotest/temp/dfn/toml/sim-nam.toml | 193 ---- autotest/autotest/temp/dfn/toml/sim-tdis.toml | 82 -- autotest/autotest/temp/dfn/toml/sln-ems.toml | 3 - autotest/autotest/temp/dfn/toml/sln-ims.toml | 285 ----- autotest/autotest/temp/dfn/toml/sln-pts.toml | 116 --- autotest/autotest/temp/dfn/toml/swf-cdb.toml | 124 --- autotest/autotest/temp/dfn/toml/swf-chd.toml | 146 --- autotest/autotest/temp/dfn/toml/swf-cxs.toml | 82 -- autotest/autotest/temp/dfn/toml/swf-dfw.toml | 98 -- .../autotest/temp/dfn/toml/swf-dis2d.toml | 139 --- .../autotest/temp/dfn/toml/swf-disv1d.toml | 198 ---- .../autotest/temp/dfn/toml/swf-disv2d.toml | 194 ---- autotest/autotest/temp/dfn/toml/swf-evp.toml | 146 --- autotest/autotest/temp/dfn/toml/swf-flw.toml | 146 --- autotest/autotest/temp/dfn/toml/swf-ic.toml | 23 - autotest/autotest/temp/dfn/toml/swf-nam.toml | 78 -- autotest/autotest/temp/dfn/toml/swf-oc.toml | 187 ---- autotest/autotest/temp/dfn/toml/swf-pcp.toml | 146 --- autotest/autotest/temp/dfn/toml/swf-sto.toml | 33 - autotest/autotest/temp/dfn/toml/swf-zdg.toml | 159 --- autotest/autotest/temp/dfn/toml/utl-ats.toml | 63 -- autotest/autotest/temp/dfn/toml/utl-hpc.toml | 44 - .../autotest/temp/dfn/toml/utl-laktab.toml | 61 -- autotest/autotest/temp/dfn/toml/utl-ncf.toml | 103 -- autotest/autotest/temp/dfn/toml/utl-obs.toml | 57 - .../autotest/temp/dfn/toml/utl-sfrtab.toml | 55 - autotest/autotest/temp/dfn/toml/utl-spc.toml | 78 -- autotest/autotest/temp/dfn/toml/utl-spca.toml | 64 -- autotest/autotest/temp/dfn/toml/utl-tas.toml | 91 -- autotest/autotest/temp/dfn/toml/utl-ts.toml | 132 --- autotest/autotest/temp/dfn/toml/utl-tvk.toml | 74 -- autotest/autotest/temp/dfn/toml/utl-tvs.toml | 74 -- autotest/autotest/temp/dfn/utl-ats.dfn | 85 -- autotest/autotest/temp/dfn/utl-hpc.dfn | 48 - autotest/autotest/temp/dfn/utl-laktab.dfn | 70 -- autotest/autotest/temp/dfn/utl-ncf.dfn | 108 -- autotest/autotest/temp/dfn/utl-obs.dfn | 118 --- autotest/autotest/temp/dfn/utl-sfrtab.dfn | 60 -- autotest/autotest/temp/dfn/utl-spc.dfn | 129 --- autotest/autotest/temp/dfn/utl-spca.dfn | 100 -- autotest/autotest/temp/dfn/utl-tas.dfn | 125 --- autotest/autotest/temp/dfn/utl-ts.dfn | 184 ---- autotest/autotest/temp/dfn/utl-tvk.dfn | 131 --- autotest/autotest/temp/dfn/utl-tvs.dfn | 129 --- 294 files changed, 54470 deletions(-) delete mode 100644 autotest/autotest/temp/dfn/chf-cdb.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-chd.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-cxs.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-dfw.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-disv1d.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-evp.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-flw.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-pcp.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-sto.dfn delete mode 100644 autotest/autotest/temp/dfn/chf-zdg.dfn delete mode 100644 autotest/autotest/temp/dfn/common.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-chfgwf.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwegwe.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwfgwe.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwfgwf.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwfgwt.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwfprt.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-gwtgwt.dfn delete mode 100644 autotest/autotest/temp/dfn/exg-olfgwf.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-adv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-cnd.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-ctp.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-dis.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-disu.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-disv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-esl.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-est.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-fmi.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-lke.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-mve.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-mwe.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-sfe.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-ssm.dfn delete mode 100644 autotest/autotest/temp/dfn/gwe-uze.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-api.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-buy.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-chd.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-chdg.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-csub.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-dis.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-disu.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-disv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-drn.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-drng.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-evt.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-evta.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-ghb.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-ghbg.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-gnc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-hfb.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-lak.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-maw.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-mvr.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-npf.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-rch.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-rcha.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-riv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-rivg.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-sfr.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-sto.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-uzf.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-vsc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-wel.dfn delete mode 100644 autotest/autotest/temp/dfn/gwf-welg.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-adv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-api.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-cnc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-dis.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-disu.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-disv.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-dsp.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-fmi.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-ist.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-lkt.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-mst.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-mvt.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-mwt.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-sft.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-src.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-ssm.dfn delete mode 100644 autotest/autotest/temp/dfn/gwt-uzt.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-cdb.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-chd.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-cxs.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-dfw.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-dis2d.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-disv1d.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-disv2d.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-evp.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-flw.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-pcp.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-sto.dfn delete mode 100644 autotest/autotest/temp/dfn/olf-zdg.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-dis.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-disv.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-fmi.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-mip.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/prt-prp.dfn delete mode 100644 autotest/autotest/temp/dfn/sim-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/sim-tdis.dfn delete mode 100644 autotest/autotest/temp/dfn/sln-ems.dfn delete mode 100644 autotest/autotest/temp/dfn/sln-ims.dfn delete mode 100644 autotest/autotest/temp/dfn/sln-pts.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-cdb.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-chd.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-cxs.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-dfw.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-dis2d.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-disv1d.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-disv2d.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-evp.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-flw.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-ic.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-nam.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-oc.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-pcp.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-sto.dfn delete mode 100644 autotest/autotest/temp/dfn/swf-zdg.dfn delete mode 100644 autotest/autotest/temp/dfn/toml-v1_1/sim-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-cdb.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-chd.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-cxs.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-dfw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-disv1d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-evp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-flw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-pcp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-sto.toml delete mode 100644 autotest/autotest/temp/dfn/toml/chf-zdg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-chfgwf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwegwe.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwe.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfgwt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwfprt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-gwtgwt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/exg-olfgwf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-adv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-cnd.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-ctp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-dis.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-disu.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-disv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-esl.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-est.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-fmi.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-lke.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-mve.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-mwe.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-sfe.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-ssm.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwe-uze.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-api.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-buy.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-chd.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-chdg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-csub.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-dis.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-disu.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-disv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-drn.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-drng.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-evt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-evta.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-ghb.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-ghbg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-gnc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-hfb.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-lak.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-maw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-mvr.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-npf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-rch.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-rcha.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-riv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-rivg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-sfr.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-sto.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-uzf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-vsc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-wel.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwf-welg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-adv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-api.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-cnc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-dis.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-disu.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-disv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-dsp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-fmi.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-ist.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-lkt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-mst.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-mvt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-mwt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-sft.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-src.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-ssm.toml delete mode 100644 autotest/autotest/temp/dfn/toml/gwt-uzt.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-cdb.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-chd.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-cxs.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-dfw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-dis2d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-disv1d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-disv2d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-evp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-flw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-pcp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-sto.toml delete mode 100644 autotest/autotest/temp/dfn/toml/olf-zdg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-dis.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-disv.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-fmi.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-mip.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/prt-prp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/sim-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/sim-tdis.toml delete mode 100644 autotest/autotest/temp/dfn/toml/sln-ems.toml delete mode 100644 autotest/autotest/temp/dfn/toml/sln-ims.toml delete mode 100644 autotest/autotest/temp/dfn/toml/sln-pts.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-cdb.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-chd.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-cxs.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-dfw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-dis2d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-disv1d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-disv2d.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-evp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-flw.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-ic.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-nam.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-oc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-pcp.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-sto.toml delete mode 100644 autotest/autotest/temp/dfn/toml/swf-zdg.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-ats.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-hpc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-laktab.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-ncf.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-obs.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-sfrtab.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-spc.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-spca.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-tas.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-ts.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-tvk.toml delete mode 100644 autotest/autotest/temp/dfn/toml/utl-tvs.toml delete mode 100644 autotest/autotest/temp/dfn/utl-ats.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-hpc.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-laktab.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-ncf.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-obs.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-sfrtab.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-spc.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-spca.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-tas.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-ts.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-tvk.dfn delete mode 100644 autotest/autotest/temp/dfn/utl-tvs.dfn diff --git a/autotest/autotest/temp/dfn/chf-cdb.dfn b/autotest/autotest/temp/dfn/chf-cdb.dfn deleted file mode 100644 index 7cfbd98f..00000000 --- a/autotest/autotest/temp/dfn/chf-cdb.dfn +++ /dev/null @@ -1,183 +0,0 @@ -# --------------------- chf cdb options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Surface Water Flow'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'critical depth boundary'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'critical depth boundary'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'critical depth boundary'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save flows to budget file -description REPLACE save_flows {'{#1}': 'critical depth boundary'} -mf6internal ipakcb - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'CDB', '{#2}': '\\ref{table:gwf-obstypetable}'} - - -# --------------------- chf cdb dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of critical depth boundaries -description REPLACE maxbound {'{#1}': 'critical depth boundary'} - - -# --------------------- chf cdb period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid idcxs width aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name idcxs -type integer -shape -tagged false -in_record true -reader urword -time_series false -longname cross section identifier -description is the identifier for the cross section specified in the CXS Package. A value of zero indicates the zero-depth-gradient calculation will use parameters for a hydraulically wide channel. -numeric_index true - -block period -name width -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname width of the zero-depth gradient boundary -description is the channel width of the zero-depth gradient boundary. If a cross section is associated with this boundary, the width will be scaled by the cross section information. 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'zero-depth-gradient boundary'} -mf6internal auxvar - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname zero-depth-gradient boundary name -description REPLACE boundname {'{#1}': 'zero-depth-gradient boundary'} diff --git a/autotest/autotest/temp/dfn/chf-chd.dfn b/autotest/autotest/temp/dfn/chf-chd.dfn deleted file mode 100644 index a178a821..00000000 --- a/autotest/autotest/temp/dfn/chf-chd.dfn +++ /dev/null @@ -1,208 +0,0 @@ -# --------------------- chf chd options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Surface Water Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'CHD head value'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'constant-head'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'constant-head'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print CHD flows to listing file -description REPLACE print_flows {'{#1}': 'constant-head'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save CHD flows to budget file -description REPLACE save_flows {'{#1}': 'constant-head'} - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'CHD', '{#2}': '\\ref{table:gwf-obstypetable}'} - - -# --------------------- chf chd dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of constant heads -description REPLACE maxbound {'{#1}': 'constant-head'} - - -# --------------------- chf chd period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid head aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name head -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname head value assigned to constant head -description is the head at the boundary. 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'constant head'} - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname constant head boundary name -description REPLACE boundname {'{#1}': 'constant head boundary'} diff --git a/autotest/autotest/temp/dfn/chf-cxs.dfn b/autotest/autotest/temp/dfn/chf-cxs.dfn deleted file mode 100644 index 0fe273aa..00000000 --- a/autotest/autotest/temp/dfn/chf-cxs.dfn +++ /dev/null @@ -1,100 +0,0 @@ -# --------------------- chf cxs options --------------------- - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'stream reach'} -mf6internal iprpak - -# --------------------- chf cxs dimensions --------------------- - -block dimensions -name nsections -type integer -reader urword -optional false -longname number of reaches -description integer value specifying the number of cross sections that will be defined. There must be NSECTIONS entries in the PACKAGEDATA block. - -block dimensions -name npoints -type integer -reader urword -optional false -longname total number of points defined for all reaches -description integer value specifying the total number of cross-section points defined for all reaches. There must be NPOINTS entries in the CROSSSECTIONDATA block. - -# --------------------- chf cxs packagedata --------------------- - -block packagedata -name packagedata -type recarray idcxs nxspoints -shape (nsections) -reader urword -longname -description - -block packagedata -name idcxs -type integer -shape -tagged false -in_record true -reader urword -longname reach number for this entry -description integer value that defines the cross section number associated with the specified PACKAGEDATA data on the line. IDCXS must be greater than zero and less than or equal to NSECTIONS. Information must be specified for every section or the program will terminate with an error. The program will also terminate with an error if information for a section is specified more than once. -numeric_index true - -block packagedata -name nxspoints -type integer -shape -tagged false -in_record true -reader urword -longname number of points used to define cross section -description integer value that defines the number of points used to define the define the shape of a section. NXSPOINTS must be greater than 1 or the program will terminate with an error. NXSPOINTS defines the number of points that must be entered for the reach in the CROSSSECTIONDATA block. The sum of NXSPOINTS for all sections must equal the NPOINTS dimension. - -# --------------------- chf cxs crosssectiondata --------------------- - -block crosssectiondata -name crosssectiondata -type recarray xfraction height manfraction -shape (npoints) -reader urword -longname -description - -block crosssectiondata -name xfraction -type double precision -shape -tagged false -in_record true -reader urword -longname fractional width -description real value that defines the station (x) data for the cross-section as a fraction of the width (WIDTH) of the reach. XFRACTION must be greater than or equal to zero but can be greater than one. XFRACTION values can be used to decrease or increase the width of a reach from the specified reach width (WIDTH). - -block crosssectiondata -name height -type double precision -shape -tagged false -in_record true -reader urword -longname depth -description real value that is the height relative to the top of the lowest elevation of the streambed (ELEVATION) and corresponding to the station data on the same line. HEIGHT must be greater than or equal to zero and at least one cross-section height must be equal to zero. - -block crosssectiondata -name manfraction -type double precision -shape -tagged false -in_record true -reader urword -optional false -longname Manning's roughness coefficient -description real value that defines the Manning's roughness coefficient data for the cross-section as a fraction of the Manning's roughness coefficient for the reach (MANNINGSN) and corresponding to the station data on the same line. MANFRACTION must be greater than zero. MANFRACTION is applied from the XFRACTION value on the same line to the XFRACTION value on the next line. diff --git a/autotest/autotest/temp/dfn/chf-dfw.dfn b/autotest/autotest/temp/dfn/chf-dfw.dfn deleted file mode 100644 index 6a16ccb5..00000000 --- a/autotest/autotest/temp/dfn/chf-dfw.dfn +++ /dev/null @@ -1,143 +0,0 @@ -# --------------------- chf dfw options --------------------- - -block options -name central_in_space -type keyword -reader urword -optional true -longname use central in space weighting -description keyword to indicate conductance should be calculated using central-in-space weighting instead of the default upstream weighting approach. This option should be used with caution as it does not work well unless all of the stream reaches are saturated. With this option, there is no way for water to flow into a dry reach from connected reaches. -mf6internal icentral - -block options -name length_conversion -type double precision -reader urword -optional true -longname length conversion factor -description real value that is used to convert user-specified Manning's roughness coefficients from meters to model length units. LENGTH\_CONVERSION should be set to 3.28081, 1.0, and 100.0 when using length units (LENGTH\_UNITS) of feet, meters, or centimeters in the simulation, respectively. LENGTH\_CONVERSION does not need to be specified if LENGTH\_UNITS are meters. -mf6internal lengthconv - -block options -name time_conversion -type double precision -reader urword -optional true -longname time conversion factor -description real value that is used to convert user-specified Manning's roughness coefficients from seconds to model time units. TIME\_CONVERSION should be set to 1.0, 60.0, 3,600.0, 86,400.0, and 31,557,600.0 when using time units (TIME\_UNITS) of seconds, minutes, hours, days, or years in the simulation, respectively. TIME\_CONVERSION does not need to be specified if TIME\_UNITS are seconds. -mf6internal timeconv - -block options -name save_flows -type keyword -reader urword -optional true -longname keyword to save DFW flows -description keyword to indicate that budget flow terms will be written to the file specified with ``BUDGET SAVE FILE'' in Output Control. -mf6internal ipakcb - -block options -name print_flows -type keyword -reader urword -optional true -longname keyword to print DFW flows to listing file -description keyword to indicate that calculated flows between cells will be printed to the listing file for every stress period time step in which ``BUDGET PRINT'' is specified in Output Control. If there is no Output Control option and ``PRINT\_FLOWS'' is specified, then flow rates are printed for the last time step of each stress period. This option can produce extremely large list files because all cell-by-cell flows are printed. It should only be used with the DFW Package for models that have a small number of cells. -mf6internal iprflow - -block options -name save_velocity -type keyword -reader urword -optional true -longname keyword to save velocity -description keyword to indicate that x, y, and z components of velocity will be calculated at cell centers and written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. If this option is activated, then additional information may be required in the discretization packages and the GWF Exchange package (if GWF models are coupled). Specifically, ANGLDEGX must be specified in the CONNECTIONDATA block of the DISU Package; ANGLDEGX must also be specified for the GWF Exchange as an auxiliary variable. -mf6internal isavvelocity - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'DFW', '{#2}': '\\ref{table:gwf-obstypetable}'} - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -# dev options - -block options -name dev_swr_conductance -type keyword -reader urword -optional true -longname use SWR conductance formulation -description use the conductance formulation in the Surface Water Routing (SWR) Process for MODFLOW-2005. -mf6internal iswrcond - -# --------------------- chf dfw griddata --------------------- - -block griddata -name manningsn -type double precision -shape (nodes) -valid -reader readarray -layered false -optional -longname mannings roughness coefficient -description mannings roughness coefficient - -block griddata -name idcxs -type integer -shape (nodes) -valid -reader readarray -layered false -optional true -longname cross section number -description integer value indication the cross section identifier in the Cross Section Package that applies to the reach. If not provided then reach will be treated as hydraulically wide. -numeric_index true diff --git a/autotest/autotest/temp/dfn/chf-disv1d.dfn b/autotest/autotest/temp/dfn/chf-disv1d.dfn deleted file mode 100644 index 7c4ff0b4..00000000 --- a/autotest/autotest/temp/dfn/chf-disv1d.dfn +++ /dev/null @@ -1,250 +0,0 @@ -# --------------------- chf disv1d options --------------------- - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position origin of the model grid coordinate system -description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position origin of the model grid coordinate system -description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -# --------------------- chf disv1d dimensions --------------------- - -block dimensions -name nodes -type integer -reader urword -optional false -longname number of linear features -description is the number of linear cells. - -block dimensions -name nvert -type integer -reader urword -optional true -longname number of columns -description is the total number of (x, y, z) vertex pairs used to characterize the model grid. - -# --------------------- chf disv1d griddata --------------------- - -block griddata -name width -type double precision -shape (nodes) -valid -reader readarray -layered false -optional -longname width -description real value that defines the width for each one-dimensional cell. WIDTH must be greater than zero. If the Cross Section (CXS) Package is used, then the WIDTH value will be multiplied by the specified cross section x-fraction values. - -block griddata -name bottom -type double precision -shape (nodes) -valid -reader readarray -layered false -optional -longname bottom elevation for the one-dimensional cell -description bottom elevation for the one-dimensional cell. - -block griddata -name idomain -type integer -shape (nodes) -reader readarray -layered false -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. - - -# --------------------- chf disv1d vertices --------------------- - -block vertices -name vertices -type recarray iv xv yv -shape (nvert) -reader urword -optional false -longname vertices data -description - -block vertices -name iv -type integer -in_record true -tagged false -reader urword -optional false -longname vertex number -description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. -numeric_index true - -block vertices -name xv -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for vertex -description is the x-coordinate for the vertex. - -block vertices -name yv -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for vertex -description is the y-coordinate for the vertex. - -# --------------------- chf disv1d cell1d --------------------- - -block cell1d -name cell1d -type recarray icell1d fdc ncvert icvert -shape (nodes) -reader urword -optional false -longname cell1d data -description - -block cell1d -name icell1d -type integer -in_record true -tagged false -reader urword -optional false -longname cell1d number -description is the cell1d number. Records in the cell1d block must be listed in consecutive order from the first to the last. -numeric_index true - -block cell1d -name fdc -type double precision -in_record true -tagged false -reader urword -optional false -longname fractional distance to the cell center -description is the fractional distance to the cell center. FDC is relative to the first vertex in the ICVERT array. In most cases FDC should be 0.5, which would place the center of the line segment that defines the cell. If the value of FDC is 1, the cell center would located at the last vertex. FDC values of 0 and 1 can be used to place the node at either end of the cell which can be useful for cells with boundary conditions. - -block cell1d -name ncvert -type integer -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. - -block cell1d -name icvert -type integer -shape (ncvert) -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in the order that defines the line representing the cell. Cells that are connected must share vertices. The bottom elevation of the cell is calculated using the ZV of the first and last vertex point and FDC. -numeric_index true diff --git a/autotest/autotest/temp/dfn/chf-evp.dfn b/autotest/autotest/temp/dfn/chf-evp.dfn deleted file mode 100644 index aadeac9b..00000000 --- a/autotest/autotest/temp/dfn/chf-evp.dfn +++ /dev/null @@ -1,211 +0,0 @@ -# --------------------- swf evp options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Surface Water Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'evaporation'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'evaporation'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'evaporation'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print evaporation rates to listing file -description REPLACE print_flows {'{#1}': 'evaporation'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save evaporation to budget file -description REPLACE save_flows {'{#1}': 'evaporation'} -mf6internal ipakcb - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'EVP', '{#2}': '\\ref{table:gwf-obstypetable}'} - -# --------------------- swf evp dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of evaporation cells -description REPLACE maxbound {'{#1}': 'evaporation'} - - -# --------------------- swf evp period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid evaporation aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name evaporation -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname evaporation rate -description is the evaporation flux rate ($LT^{-1}$). This rate is multiplied inside the program by the water surface area of the cell to calculate the volumetric evaporation rate. 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'evaporation'} -mf6internal auxvar - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname evaporation name -description REPLACE boundname {'{#1}': 'evaporation'} diff --git a/autotest/autotest/temp/dfn/chf-flw.dfn b/autotest/autotest/temp/dfn/chf-flw.dfn deleted file mode 100644 index 09cef641..00000000 --- a/autotest/autotest/temp/dfn/chf-flw.dfn +++ /dev/null @@ -1,207 +0,0 @@ -# --------------------- chf flw options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Stream Network Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'flow rate'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'inflow'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'inflow'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'inflow'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'inflow'} - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'FLW', '{#2}': '\\ref{table:gwf-obstypetable}'} - -# --------------------- chf flw dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of inflow -description REPLACE maxbound {'{#1}': 'inflow'} - - -# --------------------- chf flw period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid q aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name q -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname well rate -description is the volumetric inflow rate. A positive value indicates inflow to the stream. Negative values are not allows. 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'inflow'} - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname inflow name -description REPLACE boundname {'{#1}': 'inflow'} diff --git a/autotest/autotest/temp/dfn/chf-ic.dfn b/autotest/autotest/temp/dfn/chf-ic.dfn deleted file mode 100644 index a483e8db..00000000 --- a/autotest/autotest/temp/dfn/chf-ic.dfn +++ /dev/null @@ -1,22 +0,0 @@ -# --------------------- chf ic options --------------------- - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -# --------------------- chf ic griddata --------------------- - -block griddata -name strt -type double precision -shape (nodes) -reader readarray -layered true -longname starting concentration -description is the initial (starting) stage---that is, stage at the beginning of the CHF Model simulation. STRT must be specified for all CHF Model simulations. One value is read for every model reach. -default_value 0.0 diff --git a/autotest/autotest/temp/dfn/chf-nam.dfn b/autotest/autotest/temp/dfn/chf-nam.dfn deleted file mode 100644 index dd2b16d3..00000000 --- a/autotest/autotest/temp/dfn/chf-nam.dfn +++ /dev/null @@ -1,99 +0,0 @@ -# --------------------- chf nam options --------------------- - -block options -name list -type string -reader urword -optional true -preserve_case true -longname name of listing file -description is name of the listing file to create for this CHF model. If not specified, then the name of the list file will be the basename of the CHF model name file and the '.lst' extension. For example, if the CHF name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'all model stress package'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'all model package'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save flows for all packages to budget file -description REPLACE save_flows {'{#1}': 'all model package'} - -block options -name newtonoptions -type record newton under_relaxation -reader urword -optional true -longname newton keyword and options -description none - -block options -name newton -in_record true -type keyword -reader urword -longname keyword to activate Newton-Raphson formulation -description keyword that activates the Newton-Raphson formulation for surface water flow between connected reaches and stress packages that support calculation of Newton-Raphson terms. - -block options -name under_relaxation -in_record true -type keyword -reader urword -optional true -longname keyword to activate Newton-Raphson UNDER_RELAXATION option -description keyword that indicates whether the surface water stage in a reach will be under-relaxed when water levels fall below the bottom of the model below any given cell. By default, Newton-Raphson UNDER\_RELAXATION is not applied. - -# --------------------- chf nam packages --------------------- - -block packages -name packages -type recarray ftype fname pname -reader urword -optional false -longname package list -description - -block packages -name ftype -in_record true -type string -tagged false -reader urword -longname package type -description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-chf}. Ftype may be entered in any combination of uppercase and lowercase. - -block packages -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. - -block packages -name pname -in_record true -type string -tagged false -reader urword -optional true -longname user name for package -description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single CHF Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. - diff --git a/autotest/autotest/temp/dfn/chf-oc.dfn b/autotest/autotest/temp/dfn/chf-oc.dfn deleted file mode 100644 index 6df35aec..00000000 --- a/autotest/autotest/temp/dfn/chf-oc.dfn +++ /dev/null @@ -1,350 +0,0 @@ -# --------------------- chf oc options --------------------- - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -mf6internal budfilerec -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -mf6internal budcsvfilerec -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name qoutflow_filerecord -type record qoutflow fileout qoutflowfile -shape -reader urword -tagged true -optional true -mf6internal qoutfilerec -longname -description - -block options -name qoutflow -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname qoutflow keyword -description keyword to specify that record corresponds to qoutflow. - -block options -name qoutflowfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write conc information. - -block options -name stage_filerecord -type record stage fileout stagefile -shape -reader urword -tagged true -optional true -mf6internal stagefilerec -longname -description - -block options -name stage -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname stage keyword -description keyword to specify that record corresponds to stage. - -block options -name stagefile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write stage information. - -block options -name qoutflowprintrecord -type record qoutflow print_format formatrecord -shape -reader urword -optional true -mf6internal qoutprintrec -longname -description - -block options -name print_format -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to indicate that a print format follows -description keyword to specify format for printing to the listing file. - -block options -name formatrecord -type record columns width digits format -shape -in_record true -reader urword -tagged -optional false -longname -description - -block options -name columns -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of columns -description number of columns for writing data. - -block options -name width -type integer -shape -in_record true -reader urword -tagged true -optional -longname width for each number -description width for writing each number. - -block options -name digits -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of digits -description number of digits to use for writing a number. - -block options -name format -type string -shape -in_record true -reader urword -tagged false -optional false -longname write format -description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. - -# --------------------- chf oc period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name saverecord -type record save rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name save -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be saved this stress period. - -block period -name printrecord -type record print rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name print -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be printed this stress period. - -block period -name rtype -type string -shape -in_record true -reader urword -tagged false -optional false -longname record type -description type of information to save or print. Can be BUDGET. - -block period -name ocsetting -type keystring all first last frequency steps -shape -tagged false -in_record true -reader urword -longname -description specifies the steps for which the data will be saved. - -block period -name all -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for all time steps in period. - -block period -name first -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name last -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name frequency -type integer -shape -tagged true -in_record true -reader urword -longname -description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name steps -type integer -shape ($ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. - -block exchangedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -longname auxiliary variables -description represents the values of the auxiliary variables for each GWEGWE Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. -mf6internal auxvar - -block exchangedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname exchange boundname -description REPLACE boundname {'{#1}': 'GWE Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-gwfgwe.dfn b/autotest/autotest/temp/dfn/exg-gwfgwe.dfn deleted file mode 100644 index fe541026..00000000 --- a/autotest/autotest/temp/dfn/exg-gwfgwe.dfn +++ /dev/null @@ -1,3 +0,0 @@ -# --------------------- exg gwfgwe options --------------------- - - diff --git a/autotest/autotest/temp/dfn/exg-gwfgwf.dfn b/autotest/autotest/temp/dfn/exg-gwfgwf.dfn deleted file mode 100644 index 0f68acea..00000000 --- a/autotest/autotest/temp/dfn/exg-gwfgwf.dfn +++ /dev/null @@ -1,321 +0,0 @@ -# --------------------- exg gwfgwf options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description an array of auxiliary variable names. There is no limit on the number of auxiliary variables that can be provided. Most auxiliary variables will not be used by the GWF-GWF Exchange, but they will be available for use by other parts of the program. If an auxiliary variable with the name ``ANGLDEGX'' is found, then this information will be used as the angle (provided in degrees) between the connection face normal and the x axis, where a value of zero indicates that a normal vector points directly along the positive x axis. The connection face normal is a normal vector on the cell face shared between the cell in model 1 and the cell in model 2 pointing away from the model 1 cell. Additional information on ``ANGLDEGX'' and when it is required is provided in the description of the DISU Package. If an auxiliary variable with the name ``CDIST'' is found, then this information will be used in the calculation of specific discharge within model cells connected by the exchange. For a horizontal connection, CDIST should be specified as the horizontal distance between the cell centers, and should not include the vertical component. For vertical connections, CDIST should be specified as the difference in elevation between the two cell centers. Both ANGLDEGX and CDIST are required if specific discharge is calculated for either of the groundwater models. - - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'GWF Exchange'} - -block options -name print_input -type keyword -reader urword -optional true -longname keyword to print input to list file -description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname keyword to print gwfgwf flows to list file -description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname keyword to save GWFGWF flows -description keyword to indicate that cell-by-cell flow terms will be written to the budget file for each model provided that the Output Control for the models are set up with the ``BUDGET SAVE FILE'' option. -mf6internal ipakcb - -block options -name cell_averaging -type string -valid harmonic logarithmic amt-lmk -reader urword -optional true -longname conductance weighting option -description is a keyword and text keyword to indicate the method that will be used for calculating the conductance for horizontal cell connections. The text value for CELL\_AVERAGING can be ``HARMONIC'', ``LOGARITHMIC'', or ``AMT-LMK'', which means ``arithmetic-mean thickness and logarithmic-mean hydraulic conductivity''. If the user does not specify a value for CELL\_AVERAGING, then the harmonic-mean method will be used. - -block options -name cvoptions -type record variablecv dewatered -reader urword -optional true -longname vertical conductance options -description none - -block options -name variablecv -in_record true -type keyword -reader urword -longname keyword to activate VARIABLECV option -description keyword to indicate that the vertical conductance will be calculated using the saturated thickness and properties of the overlying cell and the thickness and properties of the underlying cell. If the DEWATERED keyword is also specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. If these keywords are not specified, then the default condition is to calculate the vertical conductance at the start of the simulation using the initial head and the cell properties. The vertical conductance remains constant for the entire simulation. - -block options -name dewatered -in_record true -type keyword -reader urword -optional true -longname keyword to activate DEWATERED option -description If the DEWATERED keyword is specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. - -block options -name newton -type keyword -reader urword -optional true -longname keyword to activate Newton-Raphson -description keyword that activates the Newton-Raphson formulation for groundwater flow between connected, convertible groundwater cells. Cells will not dry when this option is used. - -block options -name xt3d -type keyword -reader urword -optional true -longname keyword to activate XT3D -description keyword that activates the XT3D formulation between the cells connected with this GWF-GWF Exchange. - -block options -name gnc_filerecord -type record gnc6 filein gnc6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name gnc6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname gnc6 keyword -description keyword to specify that record corresponds to a ghost-node correction file. - -block options -name gnc6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname gnc6 input filename -description is the file name for ghost node correction input file. Information for the ghost nodes are provided in the file provided with these keywords. The format for specifying the ghost nodes is the same as described for the GNC Package of the GWF Model. This includes specifying OPTIONS, DIMENSIONS, and GNCDATA blocks. The order of the ghost nodes must follow the same order as the order of the cells in the EXCHANGEDATA block. For the GNCDATA, noden and all of the nodej values are assumed to be located in model 1, and nodem is assumed to be in model 2. - -block options -name mvr_filerecord -type record mvr6 filein mvr6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name mvr6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to a mover file. - -block options -name mvr6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname mvr6 input filename -description is the file name of the water mover input file to apply to this exchange. Information for the water mover are provided in the file provided with these keywords. The format for specifying the water mover information is the same as described for the Water Mover (MVR) Package of the GWF Model, with two exceptions. First, in the PACKAGES block, the model name must be included as a separate string before each package. Second, the appropriate model name must be included before package name 1 and package name 2 in the BEGIN PERIOD block. This allows providers and receivers to be located in both models listed as part of this exchange. - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwf-obstypetable} lists observation type(s) supported by the GWF-GWF package. - -block options -name dev_interfacemodel_on -type keyword -reader urword -optional true -longname activate interface model on exchange -description activates the interface model mechanism for calculating the coefficients at (and possibly near) the exchange. This keyword should only be used for development purposes. -mf6internal dev_ifmod_on - -# --------------------- exg gwfgwf dimensions --------------------- - -block dimensions -name nexg -type integer -reader urword -optional false -longname number of exchanges -description keyword and integer value specifying the number of GWF-GWF exchanges. - - -# --------------------- exg gwfgwf exchangedata --------------------- - -block exchangedata -name exchangedata -type recarray cellidm1 cellidm2 ihc cl1 cl2 hwva aux boundname -shape (nexg) -reader urword -optional false -longname exchange data -description - -block exchangedata -name cellidm1 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of first cell -description is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. -numeric_index true - -block exchangedata -name cellidm2 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of second cell -description is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. -numeric_index true - -block exchangedata -name ihc -type integer -in_record true -tagged false -reader urword -optional false -longname integer flag for connection type -description is an integer flag indicating the direction between node n and all of its m connections. If IHC = 0 then the connection is vertical. If IHC = 1 then the connection is horizontal. If IHC = 2 then the connection is horizontal for a vertically staggered grid. - -block exchangedata -name cl1 -type double precision -in_record true -tagged false -reader urword -optional false -longname connection distance -description is the distance between the center of cell 1 and the its shared face with cell 2. - -block exchangedata -name cl2 -type double precision -in_record true -tagged false -reader urword -optional false -longname connection distance -description is the distance between the center of cell 2 and the its shared face with cell 1. - -block exchangedata -name hwva -type double precision -in_record true -tagged false -reader urword -optional false -longname horizontal cell width or area for vertical flow -description is the horizontal width of the flow connection between cell 1 and cell 2 if IHC $>$ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. - -block exchangedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -longname auxiliary variables -description represents the values of the auxiliary variables for each GWFGWF Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. -mf6internal auxvar - -block exchangedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname exchange boundname -description REPLACE boundname {'{#1}': 'GWF Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-gwfgwt.dfn b/autotest/autotest/temp/dfn/exg-gwfgwt.dfn deleted file mode 100644 index 685852dd..00000000 --- a/autotest/autotest/temp/dfn/exg-gwfgwt.dfn +++ /dev/null @@ -1,3 +0,0 @@ -# --------------------- exg gwfgwt options --------------------- - - diff --git a/autotest/autotest/temp/dfn/exg-gwfprt.dfn b/autotest/autotest/temp/dfn/exg-gwfprt.dfn deleted file mode 100644 index 1008a718..00000000 --- a/autotest/autotest/temp/dfn/exg-gwfprt.dfn +++ /dev/null @@ -1,3 +0,0 @@ -# --------------------- exg gwfprt options --------------------- - - diff --git a/autotest/autotest/temp/dfn/exg-gwtgwt.dfn b/autotest/autotest/temp/dfn/exg-gwtgwt.dfn deleted file mode 100644 index 11af7496..00000000 --- a/autotest/autotest/temp/dfn/exg-gwtgwt.dfn +++ /dev/null @@ -1,281 +0,0 @@ -# --------------------- exg gwtgwt options --------------------- -# flopy multi-package - -block options -name gwfmodelname1 -type string -reader urword -optional false -longname keyword to specify name of first corresponding GWF Model -description keyword to specify name of first corresponding GWF Model. In the simulation name file, the GWT6-GWT6 entry contains names for GWT Models (exgmnamea and exgmnameb). The GWT Model with the name exgmnamea must correspond to the GWF Model with the name gwfmodelname1. - -block options -name gwfmodelname2 -type string -reader urword -optional false -longname keyword to specify name of second corresponding GWF Model -description keyword to specify name of second corresponding GWF Model. In the simulation name file, the GWT6-GWT6 entry contains names for GWT Models (exgmnamea and exgmnameb). The GWT Model with the name exgmnameb must correspond to the GWF Model with the name gwfmodelname2. - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description an array of auxiliary variable names. There is no limit on the number of auxiliary variables that can be provided. Most auxiliary variables will not be used by the GWT-GWT Exchange, but they will be available for use by other parts of the program. If an auxiliary variable with the name ``ANGLDEGX'' is found, then this information will be used as the angle (provided in degrees) between the connection face normal and the x axis, where a value of zero indicates that a normal vector points directly along the positive x axis. The connection face normal is a normal vector on the cell face shared between the cell in model 1 and the cell in model 2 pointing away from the model 1 cell. Additional information on ``ANGLDEGX'' is provided in the description of the DISU Package. ANGLDEGX must be specified if dispersion is simulated in the connected GWT models. - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'GWT Exchange'} - -block options -name print_input -type keyword -reader urword -optional true -longname keyword to print input to list file -description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname keyword to print gwfgwf flows to list file -description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname keyword to save GWFGWF flows -description keyword to indicate that cell-by-cell flow terms will be written to the budget file for each model provided that the Output Control for the models are set up with the ``BUDGET SAVE FILE'' option. -mf6internal ipakcb - -block options -name adv_scheme -type string -valid upstream central tvd -reader urword -optional true -longname advective scheme -description scheme used to solve the advection term. Can be upstream, central, or TVD. If not specified, upstream weighting is the default weighting scheme. - -block options -name dsp_xt3d_off -type keyword -shape -reader urword -optional true -longname deactivate xt3d -description deactivate the xt3d method for the dispersive flux and use the faster and less accurate approximation for this exchange. - -block options -name dsp_xt3d_rhs -type keyword -shape -reader urword -optional true -longname xt3d on right-hand side -description add xt3d dispersion terms to right-hand side, when possible, for this exchange. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name mvt_filerecord -type record mvt6 filein mvt6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name mvt6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to a transport mover file. - -block options -name mvt6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname mvt6 input filename -description is the file name of the transport mover input file to apply to this exchange. Information for the transport mover are provided in the file provided with these keywords. - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwt-obstypetable} lists observation type(s) supported by the GWT-GWT package. - -block options -name dev_interfacemodel_on -type keyword -reader urword -optional true -longname activate interface model on exchange -description activates the interface model mechanism for calculating the coefficients at (and possibly near) the exchange. This keyword should only be used for development purposes. -mf6internal dev_ifmod_on - -# --------------------- exg gwtgwt dimensions --------------------- - -block dimensions -name nexg -type integer -reader urword -optional false -longname number of exchanges -description keyword and integer value specifying the number of GWT-GWT exchanges. - - -# --------------------- exg gwtgwt exchangedata --------------------- - -block exchangedata -name exchangedata -type recarray cellidm1 cellidm2 ihc cl1 cl2 hwva aux boundname -shape (nexg) -reader urword -optional false -longname exchange data -description - -block exchangedata -name cellidm1 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of first cell -description is the cellid of the cell in model 1 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. -numeric_index true - -block exchangedata -name cellidm2 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of second cell -description is the cellid of the cell in model 2 as specified in the simulation name file. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. -numeric_index true - -block exchangedata -name ihc -type integer -in_record true -tagged false -reader urword -optional false -longname integer flag for connection type -description is an integer flag indicating the direction between node n and all of its m connections. If IHC = 0 then the connection is vertical. If IHC = 1 then the connection is horizontal. If IHC = 2 then the connection is horizontal for a vertically staggered grid. - -block exchangedata -name cl1 -type double precision -in_record true -tagged false -reader urword -optional false -longname connection distance -description is the distance between the center of cell 1 and the its shared face with cell 2. - -block exchangedata -name cl2 -type double precision -in_record true -tagged false -reader urword -optional false -longname connection distance -description is the distance between the center of cell 2 and the its shared face with cell 1. - -block exchangedata -name hwva -type double precision -in_record true -tagged false -reader urword -optional false -longname horizontal cell width or area for vertical flow -description is the horizontal width of the flow connection between cell 1 and cell 2 if IHC $>$ 0, or it is the area perpendicular to flow of the vertical connection between cell 1 and cell 2 if IHC = 0. - -block exchangedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -longname auxiliary variables -description represents the values of the auxiliary variables for each GWTGWT Exchange. The values of auxiliary variables must be present for each exchange. The values must be specified in the order of the auxiliary variables specified in the OPTIONS block. -mf6internal auxvar - -block exchangedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname exchange boundname -description REPLACE boundname {'{#1}': 'GWT Exchange'} diff --git a/autotest/autotest/temp/dfn/exg-olfgwf.dfn b/autotest/autotest/temp/dfn/exg-olfgwf.dfn deleted file mode 100644 index 7cfb29cf..00000000 --- a/autotest/autotest/temp/dfn/exg-olfgwf.dfn +++ /dev/null @@ -1,136 +0,0 @@ -# --------------------- exg olfgwf options --------------------- -# flopy multi-package - -block options -name print_input -type keyword -reader urword -optional true -longname keyword to print input to list file -description keyword to indicate that the list of exchange entries will be echoed to the listing file immediately after it is read. -mf6internal ipr_input - -block options -name print_flows -type keyword -reader urword -optional true -longname keyword to print olfgwf flows to list file -description keyword to indicate that the list of exchange flow rates will be printed to the listing file for every stress period in which ``SAVE BUDGET'' is specified in Output Control. -mf6internal ipr_flow - -block options -name fixed_conductance -type keyword -reader urword -optional true -longname keyword to indicate conductance is fixed -description keyword to indicate that the product of the bedleak and cfact input variables in the exchangedata block represents conductance. This conductance is fixed and does not change as a function of head in the surface water and groundwater models. -mf6internal ifixedcond - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description is the file name of the observations input file for this exchange. See the ``Observation utility'' section for instructions for preparing observation input files. Table \ref{table:gwf-obstypetable} lists observation type(s) supported by the SWF-GWF package. - -# --------------------- exg olfgwf dimensions --------------------- - -block dimensions -name nexg -type integer -reader urword -optional false -longname number of exchanges -description keyword and integer value specifying the number of SWF-GWF exchanges. - - -# --------------------- exg olfgwf exchangedata --------------------- - -block exchangedata -name exchangedata -type recarray cellidm1 cellidm2 bedleak cfact -shape (nexg) -reader urword -optional false -longname exchange data -description - -block exchangedata -name cellidm1 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of cell in surface water model -description is the cellid of the cell in model 1, which must be the surface water model. For a structured grid that uses the DIS input file, CELLIDM1 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM1 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM1 is the node number for the cell. -numeric_index true - -block exchangedata -name cellidm2 -type integer -in_record true -tagged false -reader urword -optional false -longname cellid of cell in groundwater model -description is the cellid of the cell in model 2, which must be the groundwater model. For a structured grid that uses the DIS input file, CELLIDM2 is the layer, row, and column numbers of the cell. For a grid that uses the DISV input file, CELLIDM2 is the layer number and CELL2D number for the two cells. If the model uses the unstructured discretization (DISU) input file, then CELLIDM2 is the node number for the cell. -numeric_index true - -block exchangedata -name bedleak -type double precision -in_record true -tagged false -reader urword -optional false -longname bed leakance -description is the leakance between the surface water and groundwater. bedleak has dimensions of 1/T and is equal to the hydraulic conductivity of the bed sediments divided by the thickness of the bed sediments. - -block exchangedata -name cfact -type double precision -in_record true -tagged false -reader urword -optional false -longname factor used for conductance calculation -description is the factor used for the conductance calculation. The definition for this parameter depends the type of surface water model and whether or not the fixed\_conductance option is specified. If the fixed\_conductance option is specified, then the hydraulic conductance is calculated as the product of bedleak and cfact. In this case, the conductance is fixed and does not change as a function of the calculated surface water and groundwater head. If the fixed\_conductance option is not specified, then the definition of cfact depends on whether the surface water model represents one-dimensional channel flow or two-dimensional overland flow. If the surface water model represents one-dimensional channel flow, then cfact is the length of the channel cell in the groundwater model cell. If the surface water model represents two-dimensional overland flow, then cfact is the intersection area of the overland flow cell and the underlying groundwater model cell. diff --git a/autotest/autotest/temp/dfn/gwe-adv.dfn b/autotest/autotest/temp/dfn/gwe-adv.dfn deleted file mode 100644 index eaa18993..00000000 --- a/autotest/autotest/temp/dfn/gwe-adv.dfn +++ /dev/null @@ -1,19 +0,0 @@ -# --------------------- gwe adv options --------------------- - -block options -name scheme -type string -valid central upstream tvd -reader urword -optional true -longname advective scheme -description scheme used to solve the advection term. Can be upstream, central, or TVD. If not specified, upstream weighting is the default weighting scheme. - - -block options -name ats_percel -type double precision -reader urword -optional true -longname fractional cell distance used for time step calculation -description fractional cell distance submitted by the ADV Package to the adaptive time stepping (ATS) package. If ATS\_PERCEL is specified and the ATS Package is active, a time step calculation will be made for each cell based on flow through the cell and cell properties. The largest time step will be calculated such that the advective fractional cell distance (ATS\_PERCEL) is not exceeded for any active cell in the grid. This time-step constraint will be submitted to the ATS Package, perhaps with constraints submitted by other packages, in the calculation of the time step. ATS\_PERCEL must be greater than zero. If a value of zero is specified for ATS\_PERCEL the program will automatically reset it to an internal no data value to indicate that time steps should not be subject to this constraint. \ No newline at end of file diff --git a/autotest/autotest/temp/dfn/gwe-cnd.dfn b/autotest/autotest/temp/dfn/gwe-cnd.dfn deleted file mode 100644 index d4b04cff..00000000 --- a/autotest/autotest/temp/dfn/gwe-cnd.dfn +++ /dev/null @@ -1,118 +0,0 @@ -# --------------------- gwe cnd options --------------------- - -block options -name xt3d_off -type keyword -shape -reader urword -optional true -longname deactivate xt3d -description deactivate the xt3d method and use the faster and less accurate approximation. This option may provide a fast and accurate solution under some circumstances, such as when flow aligns with the model grid, there is no mechanical dispersion, or when the longitudinal and transverse dispersivities are equal. This option may also be used to assess the computational demand of the XT3D approach by noting the run time differences with and without this option on. - -block options -name xt3d_rhs -type keyword -shape -reader urword -optional true -longname xt3d on right-hand side -description add xt3d terms to right-hand side, when possible. This option uses less memory, but may require more iterations. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwe cnd griddata --------------------- - -block griddata -name alh -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname longitudinal dispersivity in horizontal direction -description longitudinal dispersivity in horizontal direction. If flow is strictly horizontal, then this is the longitudinal dispersivity that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. - -block griddata -name alv -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname longitudinal dispersivity in vertical direction -description longitudinal dispersivity in vertical direction. If flow is strictly vertical, then this is the longitudinal dispsersivity value that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ALH. - -block griddata -name ath1 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity in horizontal direction -description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the second ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the y direction. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. - -block griddata -name ath2 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity in horizontal direction -description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the third ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the z direction. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH1. - -block griddata -name atv -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity when flow is in vertical direction -description transverse dispersivity when flow is in vertical direction. If flow is strictly vertical and directed in the z direction, then this value controls spreading in the x and y directions. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH2. - -block griddata -name ktw -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname thermal conductivity of the simulated fluid -description thermal conductivity of the simulated fluid. Note that the CND Package does not account for the tortuosity of the flow paths when solving for the conductive spread of heat. If tortuosity plays an important role in the thermal conductivity calculation, its effect should be reflected in the value specified for KTW. - -block griddata -name kts -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname thermal conductivity of the aquifer material -description thermal conductivity of the solid aquifer material - diff --git a/autotest/autotest/temp/dfn/gwe-ctp.dfn b/autotest/autotest/temp/dfn/gwe-ctp.dfn deleted file mode 100644 index d8930e59..00000000 --- a/autotest/autotest/temp/dfn/gwe-ctp.dfn +++ /dev/null @@ -1,213 +0,0 @@ -# --------------------- gwe ctp options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'temperature value'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'constant temperature'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'constant temperature'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'constant temperature'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save constant temperature flows to budget file -description REPLACE save_flows {'{#1}': 'constant temperature'} -mf6internal ipakcb - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname time series keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'CTP', '{#2}': '\\ref{table:gwe-obstypetable}'} - - -# --------------------- gwe ctp dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of constant temperatures -description REPLACE maxbound {'{#1}': 'constant temperature'} - - -# --------------------- gwe ctp period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid temp aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name temp -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname constant temperature value -description is the constant temperature value. 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. -mf6internal tspvar - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'constant temperature'} -mf6internal auxvar - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname constant temperature name -description REPLACE boundname {'{#1}': 'constant temperature'} diff --git a/autotest/autotest/temp/dfn/gwe-dis.dfn b/autotest/autotest/temp/dfn/gwe-dis.dfn deleted file mode 100644 index fea21df3..00000000 --- a/autotest/autotest/temp/dfn/gwe-dis.dfn +++ /dev/null @@ -1,239 +0,0 @@ -# --------------------- gwe dis options --------------------- -# mf6 subpackage utl-ncf - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position of the model grid origin -description x-position of the lower-left corner of the model grid. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position of the model grid origin -description y-position of the lower-left corner of the model grid. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the lower-left corner of the model grid. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name ncf_filerecord -type record ncf6 filein ncf6_filename -reader urword -tagged true -optional true -longname -description - -block options -name ncf6 -type keyword -in_record true -reader urword -tagged true -optional false -longname ncf keyword -description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ncf6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of NCF information -description defines a NetCDF configuration (NCF) input file. -extended true - -# --------------------- gwe dis dimensions --------------------- - -block dimensions -name nlay -type integer -reader urword -optional false -longname number of layers -description is the number of layers in the model grid. -default_value 1 - -block dimensions -name nrow -type integer -reader urword -optional false -longname number of rows -description is the number of rows in the model grid. -default_value 2 - -block dimensions -name ncol -type integer -reader urword -optional false -longname number of columns -description is the number of columns in the model grid. -default_value 2 - -# --------------------- gwe dis griddata --------------------- - -block griddata -name delr -type double precision -shape (ncol) -reader readarray -netcdf true -longname spacing along a row -description is the column spacing in the row direction. -default_value 1.0 - -block griddata -name delc -type double precision -shape (nrow) -reader readarray -netcdf true -longname spacing along a column -description is the row spacing in the column direction. -default_value 1.0 - -block griddata -name top -type double precision -shape (ncol, nrow) -reader readarray -netcdf true -longname cell top elevation -description is the top elevation for each cell in the top model layer. -default_value 1.0 - -block griddata -name botm -type double precision -shape (ncol, nrow, nlay) -reader readarray -layered true -netcdf true -longname cell bottom elevation -description is the bottom elevation for each cell. -default_value 0. - -block griddata -name idomain -type integer -shape (ncol, nrow, nlay) -reader readarray -layered true -netcdf true -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. - - diff --git a/autotest/autotest/temp/dfn/gwe-disu.dfn b/autotest/autotest/temp/dfn/gwe-disu.dfn deleted file mode 100644 index aff179ec..00000000 --- a/autotest/autotest/temp/dfn/gwe-disu.dfn +++ /dev/null @@ -1,337 +0,0 @@ -# --------------------- gwe disu options --------------------- - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position origin of the model grid coordinate system -description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position origin of the model grid coordinate system -description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name vertical_offset_tolerance -type double precision -reader urword -optional true -default_value 0.0 -longname vertical length dimension for top and bottom checking -description checks are performed to ensure that the top of a cell is not higher than the bottom of an overlying cell. This option can be used to specify the tolerance that is used for checking. If top of a cell is above the bottom of an overlying cell by a value less than this tolerance, then the program will not terminate with an error. The default value is zero. This option should generally not be used. -mf6internal voffsettol - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -# --------------------- gwe disu dimensions --------------------- - -block dimensions -name nodes -type integer -reader urword -optional false -longname number of layers -description is the number of cells in the model grid. - -block dimensions -name nja -type integer -reader urword -optional false -longname number of columns -description is the sum of the number of connections and NODES. When calculating the total number of connections, the connection between cell n and cell m is considered to be different from the connection between cell m and cell n. Thus, NJA is equal to the total number of connections, including n to m and m to n, and the total number of cells. - -block dimensions -name nvert -type integer -reader urword -optional true -longname number of vertices -description is the total number of (x, y) vertex pairs used to define the plan-view shape of each cell in the model grid. If NVERT is not specified or is specified as zero, then the VERTICES and CELL2D blocks below are not read. NVERT and the accompanying VERTICES and CELL2D blocks should be specified for most simulations. If the XT3D or SAVE\_SPECIFIC\_DISCHARGE options are specified in the NPF Package, then this information is required. - -# --------------------- gwe disu griddata --------------------- - -block griddata -name top -type double precision -shape (nodes) -reader readarray -longname cell top elevation -description is the top elevation for each cell in the model grid. - -block griddata -name bot -type double precision -shape (nodes) -reader readarray -longname cell bottom elevation -description is the bottom elevation for each cell. - -block griddata -name area -type double precision -shape (nodes) -reader readarray -longname cell surface area -description is the cell surface area (in plan view). - -block griddata -name idomain -type integer -shape (nodes) -reader readarray -layered false -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1 or greater, the cell exists in the simulation. IDOMAIN values of -1 cannot be specified for the DISU Package. - -# --------------------- gwe disu connectiondata --------------------- - -block connectiondata -name iac -type integer -shape (nodes) -reader readarray -longname number of cell connections -description is the number of connections (plus 1) for each cell. The sum of all the entries in IAC must be equal to NJA. - -block connectiondata -name ja -type integer -shape (nja) -reader readarray -longname grid connectivity -description is a list of cell number (n) followed by its connecting cell numbers (m) for each of the m cells connected to cell n. The number of values to provide for cell n is IAC(n). This list is sequentially provided for the first to the last cell. The first value in the list must be cell n itself, and the remaining cells must be listed in an increasing order (sorted from lowest number to highest). Note that the cell and its connections are only supplied for the GWE cells and their connections to the other GWE cells. Also note that the JA list input may be divided such that every node and its connectivity list can be on a separate line for ease in readability of the file. To further ease readability of the file, the node number of the cell whose connectivity is subsequently listed, may be expressed as a negative number, the sign of which is subsequently converted to positive by the code. -numeric_index true -jagged_array iac - -block connectiondata -name ihc -type integer -shape (nja) -reader readarray -longname connection type -description is an index array indicating the direction between node n and all of its m connections. If IHC = 0 then cell n and cell m are connected in the vertical direction. Cell n overlies cell m if the cell number for n is less than m; cell m overlies cell n if the cell number for m is less than n. If IHC = 1 then cell n and cell m are connected in the horizontal direction. If IHC = 2 then cell n and cell m are connected in the horizontal direction, and the connection is vertically staggered. A vertically staggered connection is one in which a cell is horizontally connected to more than one cell in a horizontal connection. -jagged_array iac - -block connectiondata -name cl12 -type double precision -shape (nja) -reader readarray -longname connection lengths -description is the array containing connection lengths between the center of cell n and the shared face with each adjacent m cell. -jagged_array iac - -block connectiondata -name hwva -type double precision -shape (nja) -reader readarray -longname connection lengths -description is a symmetric array of size NJA. For horizontal connections, entries in HWVA are the horizontal width perpendicular to flow. For vertical connections, entries in HWVA are the vertical area for flow. Thus, values in the HWVA array contain dimensions of both length and area. Entries in the HWVA array have a one-to-one correspondence with the connections specified in the JA array. Likewise, there is a one-to-one correspondence between entries in the HWVA array and entries in the IHC array, which specifies the connection type (horizontal or vertical). Entries in the HWVA array must be symmetric; the program will terminate with an error if the value for HWVA for an n to m connection does not equal the value for HWVA for the corresponding n to m connection. -jagged_array iac - -block connectiondata -name angldegx -type double precision -optional true -shape (nja) -reader readarray -longname angle of face normal to connection -description is the angle (in degrees) between the horizontal x-axis and the outward normal to the face between a cell and its connecting cells. The angle varies between zero and 360.0 degrees, where zero degrees points in the positive x-axis direction, and 90 degrees points in the positive y-axis direction. ANGLDEGX is only needed if horizontal anisotropy is specified in the NPF Package, if the XT3D option is used in the NPF Package, or if the SAVE\_SPECIFIC\_DISCHARGE option is specified in the NPF Package. ANGLDEGX does not need to be specified if these conditions are not met. ANGLDEGX is of size NJA; values specified for vertical connections and for the diagonal position are not used. Note that ANGLDEGX is read in degrees, which is different from MODFLOW-USG, which reads a similar variable (ANGLEX) in radians. -jagged_array iac - -# --------------------- gwe disu vertices --------------------- - -block vertices -name vertices -type recarray iv xv yv -shape (nvert) -reader urword -optional false -longname vertices data -description - -block vertices -name iv -type integer -in_record true -tagged false -reader urword -optional false -longname vertex number -description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. -numeric_index true - -block vertices -name xv -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for vertex -description is the x-coordinate for the vertex. - -block vertices -name yv -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for vertex -description is the y-coordinate for the vertex. - - -# --------------------- gwe disu cell2d --------------------- - -block cell2d -name cell2d -type recarray icell2d xc yc ncvert icvert -shape (nodes) -reader urword -optional false -longname cell2d data -description - -block cell2d -name icell2d -type integer -in_record true -tagged false -reader urword -optional false -longname cell2d number -description is the cell2d number. Records in the CELL2D block must be listed in consecutive order from 1 to NODES. -numeric_index true - -block cell2d -name xc -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for cell center -description is the x-coordinate for the cell center. - -block cell2d -name yc -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for cell center -description is the y-coordinate for the cell center. - -block cell2d -name ncvert -type integer -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. - -block cell2d -name icvert -type integer -shape (ncvert) -in_record true -tagged false -reader urword -optional false -longname array of vertex numbers -description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. -numeric_index true diff --git a/autotest/autotest/temp/dfn/gwe-disv.dfn b/autotest/autotest/temp/dfn/gwe-disv.dfn deleted file mode 100644 index 4ed1e0a9..00000000 --- a/autotest/autotest/temp/dfn/gwe-disv.dfn +++ /dev/null @@ -1,320 +0,0 @@ -# --------------------- gwe disv options --------------------- -# mf6 subpackage utl-ncf - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position origin of the model grid coordinate system -description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position origin of the model grid coordinate system -description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name ncf_filerecord -type record ncf6 filein ncf6_filename -reader urword -tagged true -optional true -longname -description - -block options -name ncf6 -type keyword -in_record true -reader urword -tagged true -optional false -longname ncf keyword -description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ncf6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of NCF information -description defines a NetCDF configuration (NCF) input file. -extended true - -# --------------------- gwe disv dimensions --------------------- - -block dimensions -name nlay -type integer -reader urword -optional false -longname number of layers -description is the number of layers in the model grid. - -block dimensions -name ncpl -type integer -reader urword -optional false -longname number of cells per layer -description is the number of cells per layer. This is a constant value for the grid and it applies to all layers. - -block dimensions -name nvert -type integer -reader urword -optional false -longname number of columns -description is the total number of (x, y) vertex pairs used to characterize the horizontal configuration of the model grid. - -# --------------------- gwe disv griddata --------------------- - -block griddata -name top -type double precision -shape (ncpl) -reader readarray -netcdf true -longname model top elevation -description is the top elevation for each cell in the top model layer. - -block griddata -name botm -type double precision -shape (ncpl, nlay) -reader readarray -layered true -netcdf true -longname model bottom elevation -description is the bottom elevation for each cell. - -block griddata -name idomain -type integer -shape (ncpl, nlay) -reader readarray -layered true -netcdf true -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. - - -# --------------------- gwe disv vertices --------------------- - -block vertices -name vertices -type recarray iv xv yv -shape (nvert) -reader urword -optional false -longname vertices data -description - -block vertices -name iv -type integer -in_record true -tagged false -reader urword -optional false -longname vertex number -description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. -numeric_index true - -block vertices -name xv -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for vertex -description is the x-coordinate for the vertex. - -block vertices -name yv -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for vertex -description is the y-coordinate for the vertex. - - -# --------------------- gwe disv cell2d --------------------- - -block cell2d -name cell2d -type recarray icell2d xc yc ncvert icvert -shape (ncpl) -reader urword -optional false -longname cell2d data -description - -block cell2d -name icell2d -type integer -in_record true -tagged false -reader urword -optional false -longname cell2d number -description is the CELL2D number. Records in the CELL2D block must be listed in consecutive order from the first to the last. -numeric_index true - -block cell2d -name xc -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for cell center -description is the x-coordinate for the cell center. - -block cell2d -name yc -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for cell center -description is the y-coordinate for the cell center. - -block cell2d -name ncvert -type integer -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. - -block cell2d -name icvert -type integer -shape (ncvert) -in_record true -tagged false -reader urword -optional false -longname array of vertex numbers -description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. Cells that are connected must share vertices. -numeric_index true diff --git a/autotest/autotest/temp/dfn/gwe-esl.dfn b/autotest/autotest/temp/dfn/gwe-esl.dfn deleted file mode 100644 index e12bb453..00000000 --- a/autotest/autotest/temp/dfn/gwe-esl.dfn +++ /dev/null @@ -1,211 +0,0 @@ -# --------------------- gwe esl options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'energy loading rate'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'energy source loading'} - -block options -name print_input -type keyword -reader urword -optional true -mf6internal iprpak -longname print input to listing file -description REPLACE print_input {'{#1}': 'energy source loading'} - -block options -name print_flows -type keyword -reader urword -optional true -mf6internal iprflow -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'energy source loading'} - -block options -name save_flows -type keyword -reader urword -optional true -mf6internal ipakcb -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'energy source loading'} - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'ESL', '{#2}': '\\ref{table:gwe-obstypetable}'} - -# --------------------- gwe esl dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of sources -description REPLACE maxbound {'{#1}': 'source'} - - -# --------------------- gwe esl period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid senerrate aux boundname -shape (maxbound) -reader urword -mf6internal spd -longname -description - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name senerrate -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname energy source loading rate -description is the energy source loading rate. A positive value indicates addition of energy and a negative value indicates removal of energy. 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -mf6internal auxvar -longname auxiliary variables -description REPLACE aux {'{#1}': 'energy source'} - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname well name -description REPLACE boundname {'{#1}': 'energy source'} diff --git a/autotest/autotest/temp/dfn/gwe-est.dfn b/autotest/autotest/temp/dfn/gwe-est.dfn deleted file mode 100644 index 0ce8e2c4..00000000 --- a/autotest/autotest/temp/dfn/gwe-est.dfn +++ /dev/null @@ -1,108 +0,0 @@ -# --------------------- gwe est options --------------------- - -block options -name save_flows -type keyword -reader urword -optional true -longname save calculated flows to budget file -description REPLACE save_flows {'{#1}': 'EST'} - -block options -name zero_order_decay_water -type keyword -reader urword -optional true -mf6internal ord0_decay_water -longname activate zero-order decay in aqueous phase -description is a text keyword to indicate that zero-order decay will occur in the aqueous phase. That is, decay occurs in the water and is a rate per volume of water only, not per volume of aquifer (i.e., grid cell). Use of this keyword requires that DECAY\_WATER is specified in the GRIDDATA block. - -block options -name zero_order_decay_solid -type keyword -reader urword -optional true -mf6internal ord0_decay_solid -longname activate zero-order decay in solid phase -description is a text keyword to indicate that zero-order decay will occur in the solid phase. That is, decay occurs in the solid and is a rate per mass (not volume) of solid only. Use of this keyword requires that DECAY\_SOLID is specified in the GRIDDATA block. - -block options -name density_water -type double precision -reader urword -optional true -longname density of water -description density of water used by calculations related to heat storage and conduction. This value is set to 1,000 kg/m3 if no overriding value is specified. A user-specified value should be provided for models that use units other than kilograms and meters or if it is necessary to use a value other than the default. -default_value 1000.0 -mf6internal rhow - -block options -name heat_capacity_water -type double precision -reader urword -optional true -longname heat capacity of water -description heat capacity of water used by calculations related to heat storage and conduction. This value is set to 4,184 J/kg/C if no overriding value is specified. A user-specified value should be provided for models that use units other than kilograms, joules, and degrees Celsius or it is necessary to use a value other than the default. -default_value 4184.0 -mf6internal cpw - -block options -name latent_heat_vaporization -type double precision -reader urword -optional true -longname latent heat of vaporization -description latent heat of vaporization is the amount of energy that is required to convert a given quantity of liquid into a gas and is associated with evaporative cooling. While the EST package does not simulate evaporation, multiple other packages in a GWE simulation may. To avoid having to specify the latent heat of vaporization in multiple packages, it is specified in a single location and accessed wherever it is needed. For example, evaporation may occur from the surface of streams or lakes and the energy consumed by the change in phase would be needed in both the SFE and LKE packages. This value is set to 2,453,500 J/kg if no overriding value is specified. A user-specified value should be provided for models that use units other than joules and kilograms or if it is necessary to use a value other than the default. -default_value 2453500.0 -mf6internal latheatvap - -# --------------------- gwe est griddata --------------------- - -block griddata -name porosity -type double precision -shape (nodes) -reader readarray -layered true -longname porosity -description is the mobile domain porosity, defined as the mobile domain pore volume per mobile domain volume. The GWE model does not support the concept of an immobile domain in the context of heat transport. - -block griddata -name decay_water -type double precision -shape (nodes) -reader readarray -layered true -optional true -longname aqueous phase decay rate coefficient -description is the rate coefficient for zero-order decay for the aqueous phase of the mobile domain. A negative value indicates heat (energy) production. The dimensions of zero-order decay in the aqueous phase are energy per length cubed (volume of water) per time. Zero-order decay in the aqueous phase will have no effect on simulation results unless ZERO\_ORDER\_DECAY\_WATER is specified in the options block. - -block griddata -name decay_solid -type double precision -shape (nodes) -reader readarray -layered true -optional true -longname solid phase decay rate coefficient -description is the rate coefficient for zero-order decay for the solid phase. A negative value indicates heat (energy) production. The dimensions of zero-order decay in the solid phase are energy per mass of solid per time. Zero-order decay in the solid phase will have no effect on simulation results unless ZERO\_ORDER\_DECAY\_SOLID is specified in the options block. - -block griddata -name heat_capacity_solid -type double precision -shape (nodes) -reader readarray -layered true -longname heat capacity of the aquifer material -description is the mass-based heat capacity of dry solids (aquifer material). For example, units of J/kg/C may be used (or equivalent). -mf6internal cps - -block griddata -name density_solid -type double precision -shape (nodes) -reader readarray -layered true -longname density of aquifer material -description is a user-specified value of the density of aquifer material not considering the voids. Value will remain fixed for the entire simulation. For example, if working in SI units, values may be entered as kilograms per cubic meter. -mf6internal rhos diff --git a/autotest/autotest/temp/dfn/gwe-fmi.dfn b/autotest/autotest/temp/dfn/gwe-fmi.dfn deleted file mode 100644 index 98cc2397..00000000 --- a/autotest/autotest/temp/dfn/gwe-fmi.dfn +++ /dev/null @@ -1,59 +0,0 @@ -# --------------------- gwe fmi options --------------------- - -block options -name save_flows -type keyword -reader urword -optional true -longname save calculated flow imbalance correction to budget file -description REPLACE save_flows {'{#1}': 'FMI'} - -block options -name flow_imbalance_correction -type keyword -reader urword -optional true -mf6internal imbalancecorrect -longname correct for flow imbalance -description correct for an imbalance in flows by assuming that any residual flow error comes in or leaves at the temperature of the cell. When this option is activated, the GWE Model budget written to the listing file will contain two additional entries: FLOW-ERROR and FLOW-CORRECTION. These two entries will be equal but opposite in sign. The FLOW-CORRECTION term is a mass flow that is added to offset the error caused by an imprecise flow balance. If these terms are not relatively small, the flow model should be rerun with stricter convergence tolerances. - -# --------------------- gwe fmi packagedata --------------------- - -block packagedata -name packagedata -type recarray flowtype filein fname -reader urword -optional true -longname flowtype list -description - -block packagedata -name flowtype -in_record true -type string -tagged false -reader urword -longname flow type -description is the word GWFBUDGET, GWFHEAD, GWFGRID, GWFMOVER or the name of an advanced GWF stress package from a previous model run. If GWFBUDGET is specified, then the corresponding file must be a budget file. If GWFHEAD is specified, the file must be a head file. If GWFGRID is specified, the file must be a binary grid file. If GWFMOVER is specified, the file must be a mover file. If an advanced GWF stress package name appears then the corresponding file must be the budget file saved by a LAK, SFR, MAW or UZF Package. - -block packagedata -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block packagedata -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing flows. The path to the file should be included if the file is not located in the folder where the program was run. - diff --git a/autotest/autotest/temp/dfn/gwe-ic.dfn b/autotest/autotest/temp/dfn/gwe-ic.dfn deleted file mode 100644 index d9aa11bd..00000000 --- a/autotest/autotest/temp/dfn/gwe-ic.dfn +++ /dev/null @@ -1,33 +0,0 @@ -# --------------------- gwe ic options --------------------- - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwe ic griddata --------------------- - -block griddata -name strt -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname starting temperature -description is the initial (starting) temperature---that is, the temperature at the beginning of the GWE Model simulation. STRT must be specified for all GWE Model simulations. One value is read for every model cell. -default_value 0.0 diff --git a/autotest/autotest/temp/dfn/gwe-lke.dfn b/autotest/autotest/temp/dfn/gwe-lke.dfn deleted file mode 100644 index fc3d16cd..00000000 --- a/autotest/autotest/temp/dfn/gwe-lke.dfn +++ /dev/null @@ -1,481 +0,0 @@ -# --------------------- gwe lke options --------------------- -# flopy multi-package - -block options -name flow_package_name -type string -shape -reader urword -optional true -longname keyword to specify name of corresponding flow package -description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWE name file). - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} - -block options -name flow_package_auxiliary_name -type string -shape -reader urword -optional true -longname keyword to specify name of temperature auxiliary variable in flow package -description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated temperatures from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'lake'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'lake'} - -block options -name print_temperature -type keyword -reader urword -optional true -longname print calculated temperatures to listing file -description REPLACE print_temperature {'{#1}': 'lake', '{#2}': 'temperature', '{#3}': 'TEMPERATURE'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'lake'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save lake flows to budget file -description REPLACE save_flows {'{#1}': 'lake'} - -block options -name temperature_filerecord -type record temperature fileout tempfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name temperature -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname stage keyword -description keyword to specify that record corresponds to temperature. - -block options -name tempfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write temperature information. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'LKE', '{#2}': '\\ref{table:gwe-obstypetable}'} - - -# --------------------- gwe lke packagedata --------------------- - -block packagedata -name packagedata -type recarray lakeno strt ktf rbthcnd aux boundname -shape (maxbound) -reader urword -longname -description - -block packagedata -name lakeno -type integer -shape -tagged false -in_record true -reader urword -longname lake number for this entry -description integer value that defines the lake number associated with the specified PACKAGEDATA data on the line. LAKENO must be greater than zero and less than or equal to NLAKES. Lake information must be specified for every lake or the program will terminate with an error. The program will also terminate with an error if information for a lake is specified more than once. -numeric_index true - -block packagedata -name strt -type double precision -shape -tagged false -in_record true -reader urword -longname starting lake temperature -description real value that defines the starting temperature for the lake. - -block packagedata -name ktf -type double precision -shape -tagged false -in_record true -reader urword -longname boundary thermal conductivity -description is the thermal conductivity of the material between the aquifer cell and the lake. The thickness of the material is defined by the variable RBTHCND. - -block packagedata -name rbthcnd -type double precision -shape -tagged false -in_record true -reader urword -longname streambed thickness -description real value that defines the thickness of the lakebed material through which conduction occurs. Must be greater than 0. - -block packagedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -time_series true -optional true -longname auxiliary variables -description REPLACE aux {'{#1}': 'lake'} - -block packagedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname lake name -description REPLACE boundname {'{#1}': 'lake'} - - -# --------------------- gwe lke period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name lakeperioddata -type recarray lakeno laksetting -shape -reader urword -longname -description - -block period -name lakeno -type integer -shape -tagged false -in_record true -reader urword -longname lake number for this entry -description integer value that defines the lake number associated with the specified PERIOD data on the line. LAKENO must be greater than zero and less than or equal to NLAKES. -numeric_index true - -block period -name laksetting -type keystring status temperature rainfall evaporation runoff ext-inflow auxiliaryrecord -shape -tagged false -in_record true -reader urword -longname -description line of information that is parsed into a keyword and values. Keyword values that can be used to start the LAKSETTING string include: STATUS, TEMPERATURE, RAINFALL, EVAPORATION, RUNOFF, and AUXILIARY. These settings are used to assign the temperature associated with the corresponding flow terms. Temperatures cannot be specified for all flow terms. For example, the Lake Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the lake at the calculated temperature of the lake. - -block period -name status -type string -shape -tagged true -in_record true -reader urword -longname lake temperature status -description keyword option to define lake status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that temperature will be calculated for the lake. If a lake is inactive, then there will be no energy fluxes into or out of the lake and the inactive value will be written for the lake temperature. If a lake is constant, then the temperature for the lake will be fixed at the user specified value. - -block period -name temperature -type string -shape -tagged true -in_record true -time_series true -reader urword -longname lake temperature -description real or character value that defines the temperature for the lake. The specified TEMPERATURE is only applied if the lake is a constant temperature lake. 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. - -block period -name rainfall -type string -shape -tagged true -in_record true -reader urword -time_series true -longname rainfall temperature -description real or character value that defines the rainfall temperature for the lake. 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. - -block period -name evaporation -type string -shape -tagged true -in_record true -reader urword -time_series true -longname evaporation temperature -description use of the EVAPORATION keyword is allowed in the LKE package; however, the specified value is not currently used in LKE calculations. Instead, the latent heat of evaporation is multiplied by the simulated evaporation rate for determining the thermal energy lost from a stream reach. - - -block period -name runoff -type string -shape -tagged true -in_record true -reader urword -time_series true -longname runoff temperature -description real or character value that defines the temperature of runoff for the lake. Users are free to use whatever temperature scale they want, which might include negative temperatures. 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. - -block period -name ext-inflow -type string -shape -tagged true -in_record true -reader urword -time_series true -longname ext-inflow temperature -description real or character value that defines the temperature of external inflow for the lake. Users are free to use whatever temperature scale they want, which might include negative temperatures. 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. - -block period -name auxiliaryrecord -type record auxiliary auxname auxval -shape -tagged -in_record true -reader urword -longname -description - -block period -name auxiliary -type keyword -shape -in_record true -reader urword -longname -description keyword for specifying auxiliary variable. - -block period -name auxname -type string -shape -tagged false -in_record true -reader urword -longname -description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. - -block period -name auxval -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname auxiliary variable value -description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwe-mve.dfn b/autotest/autotest/temp/dfn/gwe-mve.dfn deleted file mode 100644 index e67e76ba..00000000 --- a/autotest/autotest/temp/dfn/gwe-mve.dfn +++ /dev/null @@ -1,106 +0,0 @@ -# --------------------- gwe mve options --------------------- -# flopy subpackage mve_filerecord mve perioddata perioddata -# flopy parent_name_type parent_model_or_package MFModel/MFPackage - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'mover'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'lake'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save lake flows to budget file -description REPLACE save_flows {'{#1}': 'lake'} - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - - diff --git a/autotest/autotest/temp/dfn/gwe-mwe.dfn b/autotest/autotest/temp/dfn/gwe-mwe.dfn deleted file mode 100644 index d751bbd2..00000000 --- a/autotest/autotest/temp/dfn/gwe-mwe.dfn +++ /dev/null @@ -1,447 +0,0 @@ -# --------------------- gwe mwe options --------------------- -# flopy multi-package - -block options -name flow_package_name -type string -shape -reader urword -optional true -longname keyword to specify name of corresponding flow package -description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWE name file). - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Energy Transport'} - -block options -name flow_package_auxiliary_name -type string -shape -reader urword -optional true -longname keyword to specify name of temperature auxiliary variable in flow package -description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated temperatures from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'well'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'well'} - -block options -name print_temperature -type keyword -reader urword -optional true -longname print calculated temperatures to listing file -description REPLACE print_temperature {'{#1}': 'well', '{#2}': 'temperature', '{#3}': 'TEMPERATURE'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'well'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'well'} - -block options -name temperature_filerecord -type record temperature fileout tempfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name temperature -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname stage keyword -description keyword to specify that record corresponds to temperature. - -block options -name tempfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write temperature information. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'MWE', '{#2}': '\\ref{table:gwe-obstypetable}'} - - -# --------------------- gwe mwe packagedata --------------------- - -block packagedata -name packagedata -type recarray mawno strt ktf fthk aux boundname -shape (maxbound) -reader urword -longname -description - -block packagedata -name mawno -type integer -shape -tagged false -in_record true -reader urword -longname well number for this entry -description integer value that defines the well number associated with the specified PACKAGEDATA data on the line. MAWNO must be greater than zero and less than or equal to NMAWWELLS. Well information must be specified for every well or the program will terminate with an error. The program will also terminate with an error if information for a well is specified more than once. -numeric_index true - -block packagedata -name strt -type double precision -shape -tagged false -in_record true -reader urword -longname starting well temperature -description real value that defines the starting temperature for the well. - -block packagedata -name ktf -type double precision -shape -tagged false -in_record true -reader urword -longname thermal conductivity of the feature -description is the thermal conductivity of the material between the aquifer cell and the feature. The thickness of the material is defined by the variable FTHK. - -block packagedata -name fthk -type double precision -shape -tagged false -in_record true -reader urword -longname thickness of the well feature -description real value that defines the thickness of the material through which conduction occurs. Must be greater than 0. - -block packagedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -time_series true -optional true -longname auxiliary variables -description REPLACE aux {'{#1}': 'well'} - -block packagedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname well name -description REPLACE boundname {'{#1}': 'well'} - - -# --------------------- gwe mwe period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name mweperioddata -type recarray mawno mwesetting -shape -reader urword -longname -description - -block period -name mawno -type integer -shape -tagged false -in_record true -reader urword -longname well number for this entry -description integer value that defines the well number associated with the specified PERIOD data on the line. MAWNO must be greater than zero and less than or equal to NMAWWELLS. -numeric_index true - -block period -name mwesetting -type keystring status temperature rate auxiliaryrecord -shape -tagged false -in_record true -reader urword -longname -description line of information that is parsed into a keyword and values. Keyword values that can be used to start the MWESETTING string include: STATUS, TEMPERATURE, RAINFALL, EVAPORATION, RUNOFF, and AUXILIARY. These settings are used to assign the temperature of associated with the corresponding flow terms. Temperatures cannot be specified for all flow terms. For example, the Multi-Aquifer Well Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the well at the calculated temperature of the well. - -block period -name status -type string -shape -tagged true -in_record true -reader urword -longname well temperature status -description keyword option to define well status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that temperature will be calculated for the well. If a well is inactive, then there will be no solute mass fluxes into or out of the well and the inactive value will be written for the well temperature. If a well is constant, then the temperature for the well will be fixed at the user specified value. - -block period -name temperature -type string -shape -tagged true -in_record true -time_series true -reader urword -longname well temperature -description real or character value that defines the temperature for the well. The specified TEMPERATURE is only applied if the well is a constant temperature well. 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. - -block period -name rate -type string -shape -tagged true -in_record true -reader urword -time_series true -longname well injection temperature -description real or character value that defines the injection temperature $(e.g.,\:^{\circ}C\:or\:^{\circ}F)$ for the well. 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. - -block period -name auxiliaryrecord -type record auxiliary auxname auxval -shape -tagged -in_record true -reader urword -longname -description - -block period -name auxiliary -type keyword -shape -in_record true -reader urword -longname -description keyword for specifying auxiliary variable. - -block period -name auxname -type string -shape -tagged false -in_record true -reader urword -longname -description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. - -block period -name auxval -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname auxiliary variable value -description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwe-nam.dfn b/autotest/autotest/temp/dfn/gwe-nam.dfn deleted file mode 100644 index a3e89b0e..00000000 --- a/autotest/autotest/temp/dfn/gwe-nam.dfn +++ /dev/null @@ -1,210 +0,0 @@ -# --------------------- gwe nam options --------------------- - -block options -name list -type string -reader urword -optional true -preserve_case true -longname name of listing file -description is name of the listing file to create for this GWE model. If not specified, then the name of the list file will be the basename of the GWE model name file and the ``.lst'' extension. For example, if the GWE name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'all model stress package'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'all model package'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save flows for all packages to budget file -description REPLACE save_flows {'{#1}': 'all model package'} - -block options -name dependent_variable_scaling -type keyword -reader urword -optional true -longname flag to scale X and RHS -description flag to scale X and RHS to avoid very large positive or negative dependent variable values -mf6internal idv_scale - -block options -name nc_mesh2d_filerecord -type record netcdf_mesh2d fileout ncmesh2dfile -shape -reader urword -tagged true -optional true -longname -description NetCDF layered mesh fileout record. -mf6internal ncmesh2drec - -block options -name netcdf_mesh2d -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a layered mesh NetCDF file. -extended true - -block options -name nc_structured_filerecord -type record netcdf_structured fileout ncstructfile -shape -reader urword -tagged true -optional true -longname -description NetCDF structured fileout record. -mf6internal ncstructrec - -block options -name netcdf_structured -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a structured NetCDF file. -mf6internal netcdf_struct -extended true - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name ncmesh2dfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF ugrid layered mesh output file. -extended true - -block options -name ncstructfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF structured output file. -extended true - -block options -name nc_filerecord -type record netcdf filein netcdf_filename -reader urword -tagged true -optional true -longname -description NetCDF filerecord - -block options -name netcdf -type keyword -in_record true -reader urword -tagged true -optional false -longname netcdf keyword -description keyword to specify that record corresponds to a NetCDF input file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name netcdf_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname netcdf input filename -description defines a NetCDF input file. -mf6internal netcdf_fname -extended true - -# --------------------- gwe nam packages --------------------- - -block packages -name packages -type recarray ftype fname pname -reader urword -optional false -longname package list -description - -block packages -name ftype -in_record true -type string -tagged false -reader urword -longname package type -description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwe}. Ftype may be entered in any combination of uppercase and lowercase. - -block packages -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. - -block packages -name pname -in_record true -type string -tagged false -reader urword -optional true -longname user name for package -description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWE Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. - diff --git a/autotest/autotest/temp/dfn/gwe-oc.dfn b/autotest/autotest/temp/dfn/gwe-oc.dfn deleted file mode 100644 index 35f14794..00000000 --- a/autotest/autotest/temp/dfn/gwe-oc.dfn +++ /dev/null @@ -1,318 +0,0 @@ -# --------------------- gwe oc options --------------------- - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -mf6internal budfilerec -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -mf6internal budcsvfilerec -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name temperature_filerecord -type record temperature fileout temperaturefile -shape -reader urword -tagged true -optional true -mf6internal tempfilerec -longname -description - -block options -name temperature -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname temperature keyword -description keyword to specify that record corresponds to temperature. - -block options -name temperaturefile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -mf6internal tempfile -description name of the output file to write temperature information. - -block options -name temperatureprintrecord -type record temperature print_format formatrecord -shape -reader urword -optional true -mf6internal tempprintrec -longname -description - -block options -name print_format -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to indicate that a print format follows -description keyword to specify format for printing to the listing file. - -block options -name formatrecord -type record columns width digits format -shape -in_record true -reader urword -tagged -optional false -longname -description - -block options -name columns -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of columns -description number of columns for writing data. - -block options -name width -type integer -shape -in_record true -reader urword -tagged true -optional -longname width for each number -description width for writing each number. - -block options -name digits -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of digits -description number of digits to use for writing a number. - -block options -name format -type string -shape -in_record true -reader urword -tagged false -optional false -longname write format -description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. - - -# --------------------- gwe oc period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name saverecord -type record save rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name save -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be saved this stress period. - -block period -name printrecord -type record print rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name print -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be printed this stress period. - -block period -name rtype -type string -shape -in_record true -reader urword -tagged false -optional false -longname record type -description type of information to save or print. Can be BUDGET or TEMPERATURE. - -block period -name ocsetting -type keystring all first last frequency steps -shape -tagged false -in_record true -reader urword -longname -description specifies the steps for which the data will be saved. - -block period -name all -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for all time steps in period. - -block period -name first -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name last -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name frequency -type integer -shape -tagged true -in_record true -reader urword -longname -description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name steps -type integer -shape ($ 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'. - -block period -name shutoffrecord -type record shut_off minrate maxrate -shape -tagged -in_record true -reader urword -longname -description - -block period -name shut_off -type keyword -shape -in_record true -reader urword -longname shut off well -description keyword for activating well shut off capability. Subsequent values define the minimum and maximum pumping rate that a well must exceed to shutoff or reactivate a well, respectively, during a stress period. SHUT\_OFF is only applied to injection wells (RATE$<0$) and if HEAD\_LIMIT is specified (not set to `OFF'). If HEAD\_LIMIT is specified, SHUT\_OFF can be deactivated by specifying a minimum value equal to zero. The SHUT\_OFF option is based on the SHUT\_OFF functionality available in the MNW2~\citep{konikow2009} package for MODFLOW-2005. The SHUT\_OFF option has been included to facilitate backward compatibility with previous versions of MODFLOW but use of the RATE\_SCALING option instead of the SHUT\_OFF option is recommended. By default, SHUT\_OFF is not used. - -block period -name minrate -type double precision -shape -tagged false -in_record true -reader urword -longname minimum shutoff rate -description is the minimum rate that a well must exceed to shutoff a well during a stress period. The well will shut down during a time step if the flow rate to the well from the aquifer is less than MINRATE. If a well is shut down during a time step, reactivation of the well cannot occur until the next time step to reduce oscillations. MINRATE must be less than maxrate. - -block period -name maxrate -type double precision -shape -tagged false -in_record true -reader urword -longname maximum shutoff rate -description is the maximum rate that a well must exceed to reactivate a well during a stress period. The well will reactivate during a timestep if the well was shutdown during the previous time step and the flow rate to the well from the aquifer exceeds maxrate. Reactivation of the well cannot occur until the next time step if a well is shutdown to reduce oscillations. maxrate must be greater than MINRATE. - -block period -name rate_scalingrecord -type record rate_scaling pump_elevation scaling_length -shape -tagged -in_record true -reader urword -longname -description - -block period -name rate_scaling -type keyword -shape -in_record true -reader urword -longname rate scaling -description activate rate scaling. If RATE\_SCALING is specified, both PUMP\_ELEVATION and SCALING\_LENGTH must be specified. RATE\_SCALING cannot be used with HEAD\_LIMIT. RATE\_SCALING can be used for extraction or injection wells. For extraction wells, the extraction rate will start to decrease once the head in the well lowers to a level equal to the pump elevation plus the scaling length. If the head in the well drops below the pump elevation, then the extraction rate is calculated to be zero. For an injection well, the injection rate will begin to decrease once the head in the well rises above the specified pump elevation. If the head in the well rises above the pump elevation plus the scaling length, then the injection rate will be set to zero. - -block period -name pump_elevation -type double precision -shape -tagged false -in_record true -reader urword -longname pump elevation -description is the elevation of the multi-aquifer well pump (PUMP\_ELEVATION). PUMP\_ELEVATION should not be less than the bottom elevation (BOTTOM) of the multi-aquifer well. - -block period -name scaling_length -type double precision -shape -tagged false -in_record true -reader urword -longname -description height above the pump elevation (SCALING\_LENGTH). If the simulated well head is below this elevation (pump elevation plus the scaling length), then the pumping rate is reduced. - -block period -name auxiliaryrecord -type record auxiliary auxname auxval -shape -tagged -in_record true -reader urword -longname -description - -block period -name auxiliary -type keyword -shape -in_record true -reader urword -longname -description keyword for specifying auxiliary variable. - -block period -name auxname -type string -shape -tagged false -in_record true -reader urword -longname -description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. - -block period -name auxval -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname auxiliary variable value -description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwf-mvr.dfn b/autotest/autotest/temp/dfn/gwf-mvr.dfn deleted file mode 100644 index 62adad3b..00000000 --- a/autotest/autotest/temp/dfn/gwf-mvr.dfn +++ /dev/null @@ -1,265 +0,0 @@ -# --------------------- gwf mvr options --------------------- -# flopy subpackage mvr_filerecord mvr perioddata perioddata -# flopy parent_name_type parent_model_or_package MFModel/MFPackage - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'MVR'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'MVR'} - -block options -name modelnames -type keyword -reader urword -optional true -longname precede all package names with model names -description keyword to indicate that all package names will be preceded by the model name for the package. Model names are required when the Mover Package is used with a GWF-GWF Exchange. The MODELNAME keyword should not be used for a Mover Package that is for a single GWF Model. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -# --------------------- gwf mvr dimensions --------------------- - -block dimensions -name maxmvr -type integer -reader urword -optional false -longname maximum number of movers -description integer value specifying the maximum number of water mover entries that will specified for any stress period. - -block dimensions -name maxpackages -type integer -reader urword -optional false -longname number of packages to be used with the mover -description integer value specifying the number of unique packages that are included in this water mover input file. - - -# --------------------- gwf mvr packages --------------------- - -block packages -name packages -type recarray mname pname -reader urword -shape (npackages) -optional false -longname -description - -block packages -name mname -type string -reader urword -shape -tagged false -in_record true -optional true -longname -description name of model containing the package. Model names are assigned by the user in the simulation name file. - -block packages -name pname -type string -reader urword -shape -tagged false -in_record true -optional false -longname -description is the name of a package that may be included in a subsequent stress period block. The package name is assigned in the name file for the GWF Model. Package names are optionally provided in the name file. If they are not provided by the user, then packages are assigned a default value, which is the package acronym followed by a hyphen and the package number. For example, the first Drain Package is named DRN-1. The second Drain Package is named DRN-2, and so forth. - - -# --------------------- gwf mvr period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name perioddata -type recarray mname1 pname1 id1 mname2 pname2 id2 mvrtype value -shape (maxbound) -reader urword -longname -description - -block period -name mname1 -type string -reader urword -shape -tagged false -in_record true -optional true -longname -description name of model containing the package, PNAME1. - -block period -name pname1 -type string -shape -tagged false -in_record true -reader urword -longname provider package name -description is the package name for the provider. The package PNAME1 must be designated to provide water through the MVR Package by specifying the keyword ``MOVER'' in its OPTIONS block. - -block period -name id1 -type integer -shape -tagged false -in_record true -reader urword -longname provider reach -description is the identifier for the provider. For the standard boundary packages, the provider identifier is the number of the boundary as it is listed in the package input file. (Note that the order of these boundaries may change by stress period, which must be accounted for in the Mover Package.) So the first well has an identifier of one. The second is two, and so forth. For the advanced packages, the identifier is the reach number (SFR Package), well number (MAW Package), or UZF cell number. For the Lake Package, ID1 is the lake outlet number. Thus, outflows from a single lake can be routed to different streams, for example. -numeric_index true - -block period -name mname2 -type string -reader urword -shape -tagged false -in_record true -optional true -longname -description name of model containing the package, PNAME2. - -block period -name pname2 -type string -shape -tagged false -in_record true -reader urword -longname receiver package name -description is the package name for the receiver. The package PNAME2 must be designated to receive water from the MVR Package by specifying the keyword ``MOVER'' in its OPTIONS block. - -block period -name id2 -type integer -shape -tagged false -in_record true -reader urword -longname receiver reach -description is the identifier for the receiver. The receiver identifier is the reach number (SFR Package), Lake number (LAK Package), well number (MAW Package), or UZF cell number. -numeric_index true - -block period -name mvrtype -type string -shape -tagged false -in_record true -reader urword -longname mover type -description is the character string signifying the method for determining how much water will be moved. Supported values are ``FACTOR'' ``EXCESS'' ``THRESHOLD'' and ``UPTO''. These four options determine how the receiver flow rate, $Q_R$, is calculated. These options mirror the options defined for the cprior variable in the SFR package, with the term ``FACTOR'' being functionally equivalent to the ``FRACTION'' option for cprior. - -block period -name value -type double precision -shape -tagged false -in_record true -reader urword -longname mover value -description is the value to be used in the equation for calculating the amount of water to move. For the ``FACTOR'' option, VALUE is the $\alpha$ factor. For the remaining options, VALUE is the specified flow rate, $Q_S$. - diff --git a/autotest/autotest/temp/dfn/gwf-nam.dfn b/autotest/autotest/temp/dfn/gwf-nam.dfn deleted file mode 100644 index 2718264d..00000000 --- a/autotest/autotest/temp/dfn/gwf-nam.dfn +++ /dev/null @@ -1,226 +0,0 @@ -# --------------------- gwf nam options --------------------- - -block options -name list -type string -reader urword -optional true -preserve_case true -longname name of listing file -description is name of the listing file to create for this GWF model. If not specified, then the name of the list file will be the basename of the GWF model name file and the '.lst' extension. For example, if the GWF name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'all model stress package'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'all model package'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save flows for all packages to budget file -description REPLACE save_flows {'{#1}': 'all model package'} - -block options -name newtonoptions -type record newton under_relaxation -reader urword -optional true -longname newton keyword and options -description none - -block options -name newton -in_record true -type keyword -reader urword -longname keyword to activate Newton-Raphson formulation -description keyword that activates the Newton-Raphson formulation for groundwater flow between connected, convertible groundwater cells and stress packages that support calculation of Newton-Raphson terms for groundwater exchanges. Cells will not dry when this option is used. By default, the Newton-Raphson formulation is not applied. - -block options -name under_relaxation -in_record true -type keyword -reader urword -optional true -longname keyword to activate Newton-Raphson UNDER_RELAXATION option -description keyword that indicates whether the groundwater head in a cell will be under-relaxed when water levels fall below the bottom of the model below any given cell. By default, Newton-Raphson UNDER\_RELAXATION is not applied. - -block options -name nc_mesh2d_filerecord -type record netcdf_mesh2d fileout ncmesh2dfile -shape -reader urword -tagged true -optional true -longname -description NetCDF layered mesh fileout record. -mf6internal ncmesh2drec - -block options -name netcdf_mesh2d -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a layered mesh NetCDF file. -extended true - -block options -name nc_structured_filerecord -type record netcdf_structured fileout ncstructfile -shape -reader urword -tagged true -optional true -longname -description NetCDF structured fileout record. -mf6internal ncstructrec - -block options -name netcdf_structured -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a structured NetCDF file. -mf6internal netcdf_struct -extended true - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name ncmesh2dfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF ugrid layered mesh output file. -extended true - -block options -name ncstructfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF structured output file. -extended true - -block options -name nc_filerecord -type record netcdf filein netcdf_filename -reader urword -tagged true -optional true -longname -description NetCDF filerecord - -block options -name netcdf -type keyword -in_record true -reader urword -tagged true -optional false -longname netcdf keyword -description keyword to specify that record corresponds to a NetCDF input file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name netcdf_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname netcdf input filename -description defines a NetCDF input file. -mf6internal netcdf_fname -extended true - -# --------------------- gwf nam packages --------------------- - -block packages -name packages -type recarray ftype fname pname -reader urword -optional false -longname package list -description - -block packages -name ftype -in_record true -type string -tagged false -reader urword -longname package type -description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwf}. Ftype may be entered in any combination of uppercase and lowercase. - -block packages -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. - -block packages -name pname -in_record true -type string -tagged false -reader urword -optional true -longname user name for package -description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWF Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. - diff --git a/autotest/autotest/temp/dfn/gwf-npf.dfn b/autotest/autotest/temp/dfn/gwf-npf.dfn deleted file mode 100644 index 3626b812..00000000 --- a/autotest/autotest/temp/dfn/gwf-npf.dfn +++ /dev/null @@ -1,375 +0,0 @@ -# --------------------- gwf npf options --------------------- -# mf6 subpackage utl-tvk - -block options -name save_flows -type keyword -reader urword -optional true -longname keyword to save NPF flows -description keyword to indicate that budget flow terms will be written to the file specified with ``BUDGET SAVE FILE'' in Output Control. -mf6internal ipakcb - -block options -name print_flows -type keyword -reader urword -optional true -longname keyword to print NPF flows to listing file -description keyword to indicate that calculated flows between cells will be printed to the listing file for every stress period time step in which ``BUDGET PRINT'' is specified in Output Control. If there is no Output Control option and ``PRINT\_FLOWS'' is specified, then flow rates are printed for the last time step of each stress period. This option can produce extremely large list files because all cell-by-cell flows are printed. It should only be used with the NPF Package for models that have a small number of cells. -mf6internal iprflow - -block options -name alternative_cell_averaging -type string -valid logarithmic amt-lmk amt-hmk -reader urword -optional true -longname conductance weighting option -description is a text keyword to indicate that an alternative method will be used for calculating the conductance for horizontal cell connections. The text value for ALTERNATIVE\_CELL\_AVERAGING can be ``LOGARITHMIC'', ``AMT-LMK'', or ``AMT-HMK''. ``AMT-LMK'' signifies that the conductance will be calculated using arithmetic-mean thickness and logarithmic-mean hydraulic conductivity. ``AMT-HMK'' signifies that the conductance will be calculated using arithmetic-mean thickness and harmonic-mean hydraulic conductivity. If the user does not specify a value for ALTERNATIVE\_CELL\_AVERAGING, then the harmonic-mean method will be used. This option cannot be used if the XT3D option is invoked. The AMT-HMK ALTERNATIVE\_CELL\_AVERAGING option, in combination with the DRY\_CELL\_SATURATION option, can be used to calculate the same horizontal conductance as MODFLOW-USG when upstream weighting is used (LAYCON=4). -mf6internal cellavg - -block options -name thickstrt -type keyword -reader urword -optional true -longname keyword to activate THICKSTRT option -description indicates that cells having a negative ICELLTYPE are confined, and their cell thickness for conductance calculations will be computed as STRT-BOT rather than TOP-BOT. This option should be used with caution as it only affects conductance calculations in the NPF Package. -mf6internal ithickstrt - -block options -name cvoptions -type record variablecv dewatered -reader urword -optional true -longname vertical conductance options -description none - -block options -name variablecv -in_record true -type keyword -reader urword -longname keyword to activate VARIABLECV option -description keyword to indicate that the vertical conductance will be calculated using the saturated thickness and properties of the overlying cell and the thickness and properties of the underlying cell. If the DEWATERED keyword is also specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. If these keywords are not specified, then the default condition is to calculate the vertical conductance at the start of the simulation using the initial head and the cell properties. The vertical conductance remains constant for the entire simulation. -mf6internal ivarcv - -block options -name dewatered -in_record true -type keyword -reader urword -optional true -longname keyword to activate DEWATERED option -description If the DEWATERED keyword is specified, then the vertical conductance is calculated using only the saturated thickness and properties of the overlying cell if the head in the underlying cell is below its top. -mf6internal idewatcv - -block options -name perched -type keyword -reader urword -optional true -longname keyword to activate PERCHED option -description keyword to indicate that when a cell is overlying a dewatered convertible cell, the head difference used in Darcy's Law is equal to the head in the overlying cell minus the bottom elevation of the overlying cell. If not specified, then the default is to use the head difference between the two cells. -mf6internal iperched - -block options -name rewet_record -type record rewet wetfct iwetit ihdwet -reader urword -optional true -longname -description - -block options -name rewet -type keyword -in_record true -reader urword -optional false -longname keyword to activate rewetting -description activates model rewetting. Rewetting is off by default. -mf6internal irewet - -block options -name wetfct -type double precision -in_record true -reader urword -optional false -longname wetting factor to use for rewetting -description is a keyword and factor that is included in the calculation of the head that is initially established at a cell when that cell is converted from dry to wet. - -block options -name iwetit -type integer -in_record true -reader urword -optional false -longname interval to use for rewetting -description is a keyword and iteration interval for attempting to wet cells. Wetting is attempted every IWETIT iteration. This applies to outer iterations and not inner iterations. If IWETIT is specified as zero or less, then the value is changed to 1. - -block options -name ihdwet -type integer -in_record true -reader urword -optional false -longname flag to determine wetting equation -description is a keyword and integer flag that determines which equation is used to define the initial head at cells that become wet. If IHDWET is 0, h = BOT + WETFCT (hm - BOT). If IHDWET is not 0, h = BOT + WETFCT (THRESH). - -block options -name xt3doptions -type record xt3d rhs -reader urword -optional true -longname keyword to activate XT3D -description none - -block options -name xt3d -in_record true -type keyword -reader urword -longname keyword to activate XT3D -description keyword indicating that the XT3D formulation will be used. If the RHS keyword is also included, then the XT3D additional terms will be added to the right-hand side. If the RHS keyword is excluded, then the XT3D terms will be put into the coefficient matrix. Use of XT3D will substantially increase the computational effort, but will result in improved accuracy for anisotropic conductivity fields and for unstructured grids in which the CVFD requirement is violated. XT3D requires additional information about the shapes of grid cells. If XT3D is active and the DISU Package is used, then the user will need to provide in the DISU Package the angldegx array in the CONNECTIONDATA block and the VERTICES and CELL2D blocks. -mf6internal ixt3d - -block options -name rhs -in_record true -type keyword -reader urword -optional true -longname keyword to XT3D on right hand side -description If the RHS keyword is also included, then the XT3D additional terms will be added to the right-hand side. If the RHS keyword is excluded, then the XT3D terms will be put into the coefficient matrix. -mf6internal ixt3drhs - -block options -name highest_cell_saturation -type keyword -reader urword -optional true -longname keyword to activate HIGHEST_CELL_SATURATION option -description keyword indicating that the maximum cell bottom will be used to calculate the saturation used to calculate the horizontal conductance between cells. This option is intended to prevent flow from leaving a dry cell and is based on~\cite{painter2008robust}. This option is only applied when the Newton-Raphson formulation is used, A warning will be issued if this option is specified and the Newton-Raphson formulation is not specified in the GWF name file. This option, in combination with the AMT-HMK ALTERNATIVE\_CELL\_AVERAGING option, can be used to calculate the same horizontal conductance as MODFLOW-USG when upstream weighting is used (LAYCON=4). -mf6internal ihighcellsat - - -block options -name save_specific_discharge -type keyword -reader urword -optional true -longname keyword to save specific discharge -description keyword to indicate that x, y, and z components of specific discharge will be calculated at cell centers and written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. If this option is activated, then additional information may be required in the discretization packages and the GWF Exchange package (if GWF models are coupled). Specifically, ANGLDEGX must be specified in the CONNECTIONDATA block of the DISU Package; ANGLDEGX must also be specified for the GWF Exchange as an auxiliary variable. -mf6internal isavspdis - -block options -name save_saturation -type keyword -reader urword -optional true -longname keyword to save saturation -description keyword to indicate that cell saturation will be written to the budget file, which is specified with ``BUDGET SAVE FILE'' in Output Control. Saturation will be saved to the budget file as an auxiliary variable saved with the DATA-SAT text label. Saturation is a cell variable that ranges from zero to one and can be used by post processing programs to determine how much of a cell volume is saturated. If ICELLTYPE is 0, then saturation is always one. -mf6internal isavsat - -block options -name k22overk -type keyword -reader urword -optional true -longname keyword to indicate that specified K22 is a ratio -description keyword to indicate that specified K22 is a ratio of K22 divided by K. If this option is specified, then the K22 array entered in the NPF Package will be multiplied by K after being read. -mf6internal ik22overk - -block options -name k33overk -type keyword -reader urword -optional true -longname keyword to indicate that specified K33 is a ratio -description keyword to indicate that specified K33 is a ratio of K33 divided by K. If this option is specified, then the K33 array entered in the NPF Package will be multiplied by K after being read. -mf6internal ik33overk - -block options -name tvk_filerecord -type record tvk6 filein tvk6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name tvk6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname tvk keyword -description keyword to specify that record corresponds to a time-varying hydraulic conductivity (TVK) file. The behavior of TVK and a description of the input file is provided separately. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name tvk6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of TVK information -description defines a time-varying hydraulic conductivity (TVK) input file. Records in the TVK file can be used to change hydraulic conductivity properties at specified times or stress periods. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# dev options - -block options -name dev_no_newton -type keyword -reader urword -optional true -longname turn off Newton for unconfined cells -description turn off Newton for unconfined cells -mf6internal inewton - -block options -name dev_omega -type double precision -reader urword -optional true -longname set saturation omega value -description set saturation omega value -mf6internal satomega - -# --------------------- gwf npf griddata --------------------- - -block griddata -name icelltype -type integer -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional -longname confined or convertible indicator -description flag for each cell that specifies how saturated thickness is treated. 0 means saturated thickness is held constant; $>$0 means saturated thickness varies with computed head when head is below the cell top; $<$0 means saturated thickness varies with computed head unless the THICKSTRT option is in effect. When THICKSTRT is in effect, a negative value for ICELLTYPE indicates that the saturated thickness value used in conductance calculations in the NPF Package will be computed as STRT-BOT and held constant. If the THICKSTRT option is not in effect, then negative values provided by the user for ICELLTYPE are automatically reassigned by the program to a value of one. -default_value 0 - -block griddata -name k -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional -longname hydraulic conductivity (L/T) -description is the hydraulic conductivity. For the common case in which the user would like to specify the horizontal hydraulic conductivity and the vertical hydraulic conductivity, then K should be assigned as the horizontal hydraulic conductivity, K33 should be assigned as the vertical hydraulic conductivity, and K22 and the three rotation angles should not be specified. When more sophisticated anisotropy is required, then K corresponds to the K11 hydraulic conductivity axis. All included cells (IDOMAIN $>$ 0) must have a K value greater than zero. -default_value 1.0 - -block griddata -name k22 -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname hydraulic conductivity of second ellipsoid axis -description is the hydraulic conductivity of the second ellipsoid axis (or the ratio of K22/K if the K22OVERK option is specified); for an unrotated case this is the hydraulic conductivity in the y direction. If K22 is not included in the GRIDDATA block, then K22 is set equal to K. For a regular MODFLOW grid (DIS Package is used) in which no rotation angles are specified, K22 is the hydraulic conductivity along columns in the y direction. For an unstructured DISU grid, the user must assign principal x and y axes and provide the angle for each cell face relative to the assigned x direction. All included cells (IDOMAIN $>$ 0) must have a K22 value greater than zero. - -block griddata -name k33 -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname hydraulic conductivity of third ellipsoid axis (L/T) -description is the hydraulic conductivity of the third ellipsoid axis (or the ratio of K33/K if the K33OVERK option is specified); for an unrotated case, this is the vertical hydraulic conductivity. When anisotropy is applied, K33 corresponds to the K33 tensor component. All included cells (IDOMAIN $>$ 0) must have a K33 value greater than zero. - -block griddata -name angle1 -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname first anisotropy rotation angle (degrees) -description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the first of three sequential rotations of the hydraulic conductivity ellipsoid. With the K11, K22, and K33 axes of the ellipsoid initially aligned with the x, y, and z coordinate axes, respectively, ANGLE1 rotates the ellipsoid about its K33 axis (within the x - y plane). A positive value represents counter-clockwise rotation when viewed from any point on the positive K33 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K11 axis lies within the x - z plane. If ANGLE1 is not specified, default values of zero are assigned to ANGLE1, ANGLE2, and ANGLE3, in which case the K11, K22, and K33 axes are aligned with the x, y, and z axes, respectively. - -block griddata -name angle2 -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname second anisotropy rotation angle (degrees) -description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the second of three sequential rotations of the hydraulic conductivity ellipsoid. Following the rotation by ANGLE1 described above, ANGLE2 rotates the ellipsoid about its K22 axis (out of the x - y plane). An array can be specified for ANGLE2 only if ANGLE1 is also specified. A positive value of ANGLE2 represents clockwise rotation when viewed from any point on the positive K22 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K11 axis lies within the x - y plane. If ANGLE2 is not specified, default values of zero are assigned to ANGLE2 and ANGLE3; connections that are not user-designated as vertical are assumed to be strictly horizontal (that is, to have no z component to their orientation); and connection lengths are based on horizontal distances. - -block griddata -name angle3 -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname third anisotropy rotation angle (degrees) -description is a rotation angle of the hydraulic conductivity tensor in degrees. The angle represents the third of three sequential rotations of the hydraulic conductivity ellipsoid. Following the rotations by ANGLE1 and ANGLE2 described above, ANGLE3 rotates the ellipsoid about its K11 axis. An array can be specified for ANGLE3 only if ANGLE1 and ANGLE2 are also specified. An array must be specified for ANGLE3 if ANGLE2 is specified. A positive value of ANGLE3 represents clockwise rotation when viewed from any point on the positive K11 axis, looking toward the center of the ellipsoid. A value of zero indicates that the K22 axis lies within the x - y plane. - -block griddata -name wetdry -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional true -longname wetdry threshold and factor -description is a combination of the wetting threshold and a flag to indicate which neighboring cells can cause a cell to become wet. If WETDRY $<$ 0, only a cell below a dry cell can cause the cell to become wet. If WETDRY $>$ 0, the cell below a dry cell and horizontally adjacent cells can cause a cell to become wet. If WETDRY is 0, the cell cannot be wetted. The absolute value of WETDRY is the wetting threshold. When the sum of BOT and the absolute value of WETDRY at a dry cell is equaled or exceeded by the head at an adjacent cell, the cell is wetted. WETDRY must be specified if ``REWET'' is specified in the OPTIONS block. If ``REWET'' is not specified in the options block, then WETDRY can be entered, and memory will be allocated for it, even though it is not used. diff --git a/autotest/autotest/temp/dfn/gwf-oc.dfn b/autotest/autotest/temp/dfn/gwf-oc.dfn deleted file mode 100644 index f4eb2803..00000000 --- a/autotest/autotest/temp/dfn/gwf-oc.dfn +++ /dev/null @@ -1,317 +0,0 @@ -# --------------------- gwf oc options --------------------- - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -mf6internal budfilerec -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -mf6internal budcsvfilerec -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name head_filerecord -type record head fileout headfile -shape -reader urword -tagged true -optional true -mf6internal headfilerec -longname -description - -block options -name head -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to head. - -block options -name headfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write head information. - -block options -name headprintrecord -type record head print_format formatrecord -shape -reader urword -optional true -mf6internal headprintrec -longname -description - -block options -name print_format -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to indicate that a print format follows -description keyword to specify format for printing to the listing file. - -block options -name formatrecord -type record columns width digits format -shape -in_record true -reader urword -tagged -optional false -longname -description - -block options -name columns -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of columns -description number of columns for writing data. - -block options -name width -type integer -shape -in_record true -reader urword -tagged true -optional -longname width for each number -description width for writing each number. - -block options -name digits -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of digits -description number of digits to use for writing a number. - -block options -name format -type string -shape -in_record true -reader urword -tagged false -optional false -longname write format -description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. - - -# --------------------- gwf oc period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name saverecord -type record save rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name save -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be saved this stress period. - -block period -name printrecord -type record print rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name print -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be printed this stress period. - -block period -name rtype -type string -shape -in_record true -reader urword -tagged false -optional false -longname record type -description type of information to save or print. Can be BUDGET or HEAD. - -block period -name ocsetting -type keystring all first last frequency steps -shape -tagged false -in_record true -reader urword -longname -description specifies the steps for which the data will be saved. - -block period -name all -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for all time steps in period. - -block period -name first -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name last -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name frequency -type integer -shape -tagged true -in_record true -reader urword -longname -description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name steps -type integer -shape (0) when the cell is under confined conditions (head greater than or equal to the top of the cell). This option has no effect on cells that are marked as being always confined (ICONVERT=0). This option is identical to the approach used to calculate storage changes under confined conditions in MODFLOW-2005. - -block options -name tvs_filerecord -type record tvs6 filein tvs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name tvs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname tvs keyword -description keyword to specify that record corresponds to a time-varying storage (TVS) file. The behavior of TVS and a description of the input file is provided separately. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name tvs6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of TVS information -description defines a time-varying storage (TVS) input file. Records in the TVS file can be used to change specific storage and specific yield properties at specified times or stress periods. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input grid arrays, which already support the layered keyword, should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# dev options -block options -name dev_original_specific_storage -type keyword -reader urword -optional true -longname development option for original specific storage -description flag indicating the original storage specific storage formulation should be used -mf6internal iorig_ss - -block options -name dev_oldstorageformulation -type keyword -reader urword -optional true -longname development option flag for old storage formulation -description development option flag for old storage formulation -mf6internal iconf_ss - -# --------------------- gwf sto griddata --------------------- - -block griddata -name iconvert -type integer -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional false -longname convertible indicator -description is a flag for each cell that specifies whether or not a cell is convertible for the storage calculation. 0 indicates confined storage is used. $>$0 indicates confined storage is used when head is above cell top and a mixed formulation of unconfined and confined storage is used when head is below cell top. -default_value 0 - -block griddata -name ss -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional false -longname specific storage -description is specific storage (or the storage coefficient if STORAGECOEFFICIENT is specified as an option). Specific storage values must be greater than or equal to 0. If the CSUB Package is included in the GWF model, specific storage must be zero for every cell. -default_value 1.e-5 - -block griddata -name sy -type double precision -shape (nodes) -valid -reader readarray -layered true -netcdf true -optional false -longname specific yield -description is specific yield. Specific yield values must be greater than or equal to 0. Specific yield does not have to be specified if there are no convertible cells (ICONVERT=0 in every cell). -default_value 0.15 - - -# --------------------- gwf sto period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name steady-state -type keyword -shape -valid -reader urword -optional true -longname steady state indicator -description keyword to indicate that stress period IPER is steady-state. Steady-state conditions will apply until the TRANSIENT keyword is specified in a subsequent BEGIN PERIOD block. If the CSUB Package is included in the GWF model, only the first and last stress period can be steady-state. -mf6internal steady_state - -block period -name transient -type keyword -shape -valid -reader urword -optional true -longname transient indicator -description keyword to indicate that stress period IPER is transient. Transient conditions will apply until the STEADY-STATE keyword is specified in a subsequent BEGIN PERIOD block. diff --git a/autotest/autotest/temp/dfn/gwf-uzf.dfn b/autotest/autotest/temp/dfn/gwf-uzf.dfn deleted file mode 100644 index 76ba773b..00000000 --- a/autotest/autotest/temp/dfn/gwf-uzf.dfn +++ /dev/null @@ -1,611 +0,0 @@ -# --------------------- gwf uzf options --------------------- -# flopy multi-package -# package-type advanced-stress-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'GWF cell area used by UZF cell'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'UZF'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'UZF'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'UZF'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'UZF'} - -block options -name wc_filerecord -type record water_content fileout wcfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name water_content -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname water_content keyword -description keyword to specify that record corresponds to unsaturated zone water contents. - -block options -name wcfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write water content information. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -preserve_case true -type string -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name package_convergence_filerecord -type record package_convergence fileout package_convergence_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name package_convergence -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname package_convergence keyword -description keyword to specify that record corresponds to the package convergence comma spaced values file. - -block options -name package_convergence_filename -type string -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma spaced values output file to write package convergence information. - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'UZF', '{#2}': '\\ref{table:gwf-obstypetable}'} - -block options -name mover -type keyword -tagged true -reader urword -optional true -longname -description REPLACE mover {'{#1}': 'UZF'} - -block options -name simulate_et -type keyword -tagged true -reader urword -optional true -longname -description keyword specifying that ET in the unsaturated (UZF) and saturated zones (GWF) will be simulated. ET can be simulated in the UZF cell and not the GWF cell by omitting keywords LINEAR\_GWET and SQUARE\_GWET. - -block options -name linear_gwet -type keyword -tagged true -reader urword -optional true -longname use linear evapotranspiration -description keyword specifying that groundwater ET will be simulated using the original ET formulation of MODFLOW-2005. - -block options -name square_gwet -type keyword -tagged true -reader urword -optional true -longname use square evapotranspiration -description keyword specifying that groundwater ET will be simulated by assuming a constant ET rate for groundwater levels between land surface (TOP) and land surface minus the ET extinction depth (TOP-EXTDP). Groundwater ET is smoothly reduced from the PET rate to zero over a nominal interval at TOP-EXTDP. - -block options -name simulate_gwseep -type keyword -tagged true -reader urword -optional true -deprecated 6.5.0 -longname activate seepage -description keyword specifying that groundwater discharge (GWSEEP) to land surface will be simulated. Groundwater discharge is nonzero when groundwater head is greater than land surface. This option is no longer recommended; a better approach is to use the Drain Package with discharge scaling as a way to handle seepage to land surface. The Drain Package with discharge scaling is described in Chapter 3 of the Supplemental Technical Information. - -block options -name unsat_etwc -type keyword -tagged true -reader urword -optional true -longname use PET for theta greater than extwc -description keyword specifying that ET in the unsaturated zone will be simulated as a function of the specified PET rate while the water content (THETA) is greater than the ET extinction water content (EXTWC). - -block options -name unsat_etae -type keyword -tagged true -reader urword -optional true -longname use root potential -description keyword specifying that ET in the unsaturated zone will be simulated using a capillary pressure based formulation. Capillary pressure is calculated using the Brooks-Corey retention function. - - -# --------------------- gwf uzf dimensions --------------------- - -block dimensions -name nuzfcells -type integer -reader urword -optional false -longname number of UZF cells -description is the number of UZF cells. More than one UZF cell can be assigned to a GWF cell; however, only one GWF cell can be assigned to a single UZF cell. If more than one UZF cell is assigned to a GWF cell, then an auxiliary variable should be used to reduce the surface area of the UZF cell with the AUXMULTNAME option. - -block dimensions -name ntrailwaves -type integer -reader urword -optional false -longname number of trailing waves -description is the number of trailing waves. A recommended value of 7 can be used for NTRAILWAVES. This value can be increased to lower mass balance error in the unsaturated zone. -default_value 7 - -block dimensions -name nwavesets -type integer -reader urword -optional false -longname number of wave sets -description is the number of wave sets. A recommended value of 40 can be used for NWAVESETS. This value can be increased if more waves are required to resolve variations in water content within the unsaturated zone. -default_value 40 - -# --------------------- gwf uzf packagedata --------------------- - -block packagedata -name packagedata -type recarray ifno cellid landflag ivertcon surfdep vks thtr thts thti eps boundname -shape (nuzfcells) -reader urword -longname -description - -block packagedata -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname uzf id number for this entry -description integer value that defines the feature (UZF object) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NUZFCELLS. UZF information must be specified for every UZF cell or the program will terminate with an error. The program will also terminate with an error if information for a UZF cell is specified more than once. -numeric_index true - -block packagedata -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block packagedata -name landflag -type integer -shape -tagged false -in_record true -reader urword -longname land flag -description integer value set to one for land surface cells indicating that boundary conditions can be applied and data can be specified in the PERIOD block. A value of 0 specifies a non-land surface cell. - -block packagedata -name ivertcon -type integer -shape -tagged false -in_record true -reader urword -longname vertical connection flag -description integer value set to specify underlying UZF cell that receives water flowing to bottom of cell. If unsaturated zone flow reaches the water table before the cell bottom, then water is added to the GWF cell instead of flowing to the underlying UZF cell. A value of 0 indicates the UZF cell is not connected to an underlying UZF cell. -numeric_index true - -block packagedata -name surfdep -type double precision -shape -tagged false -in_record true -reader urword -longname surface depression depth -description is the surface depression depth of the UZF cell. - -block packagedata -name vks -type double precision -shape -tagged false -in_record true -reader urword -longname vertical saturated hydraulic conductivity -description is the saturated vertical hydraulic conductivity of the UZF cell. This value is used with the Brooks-Corey function and the simulated water content to calculate the partially saturated hydraulic conductivity. - -block packagedata -name thtr -type double precision -shape -tagged false -in_record true -reader urword -longname residual water content -description is the residual (irreducible) water content of the UZF cell. This residual water is not available to plants and will not drain into underlying aquifer cells. - -block packagedata -name thts -type double precision -shape -tagged false -in_record true -reader urword -longname saturated water content -description is the saturated water content of the UZF cell. The values for saturated and residual water content should be set in a manner that is consistent with the specific yield value specified in the Storage Package. The saturated water content must be greater than the residual content. - -block packagedata -name thti -type double precision -shape -tagged false -in_record true -reader urword -longname initial water content -description is the initial water content of the UZF cell. The value must be greater than or equal to the residual water content and less than or equal to the saturated water content. - -block packagedata -name eps -type double precision -shape -tagged false -in_record true -reader urword -longname Brooks-Corey exponent -description is the exponent used in the Brooks-Corey function. The Brooks-Corey function is used by UZF to calculated hydraulic conductivity under partially saturated conditions as a function of water content and the user-specified saturated hydraulic conductivity. - -block packagedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname well name -description REPLACE boundname {'{#1}': 'UZF cell'} - - -# --------------------- gwf uzf period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name perioddata -type recarray ifno finf pet extdp extwc ha hroot rootact aux -shape -reader urword -longname -description - -block period -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname UZF id number -description integer value that defines the feature (UZF object) number associated with the specified PERIOD data on the line. -numeric_index true - -block period -name finf -type string -shape -tagged false -in_record true -time_series true -reader urword -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. - -block period -name pet -type string -shape -tagged false -in_record true -reader urword -time_series true -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. - -block period -name extdp -type string -shape -tagged false -in_record true -reader urword -time_series true -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. - -block period -name extwc -type string -shape -tagged false -in_record true -reader urword -time_series true -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. - -block period -name ha -type string -shape -tagged false -in_record true -time_series true -reader urword -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. - -block period -name hroot -type string -shape -tagged false -in_record true -reader urword -time_series true -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. - -block period -name rootact -type string -shape -tagged false -in_record true -reader urword -time_series true -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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -time_series true -optional true -longname auxiliary variables -description REPLACE aux {'{#1}': 'UZF'} diff --git a/autotest/autotest/temp/dfn/gwf-vsc.dfn b/autotest/autotest/temp/dfn/gwf-vsc.dfn deleted file mode 100644 index 9c0f93e6..00000000 --- a/autotest/autotest/temp/dfn/gwf-vsc.dfn +++ /dev/null @@ -1,177 +0,0 @@ -# --------------------- gwf vsc options --------------------- - -block options -name viscref -type double precision -reader urword -optional true -longname reference viscosity -description fluid reference viscosity used in the equation of state. This value is set to 1.0 if not specified as an option. -default_value 1.0 - -block options -name temperature_species_name -type string -shape -reader urword -optional true -mf6internal temp_specname -longname auxspeciesname that corresponds to temperature -description string used to identify the auxspeciesname in PACKAGEDATA that corresponds to the temperature species. There can be only one occurrence of this temperature species name in the PACKAGEDATA block or the program will terminate with an error. This value has no effect if viscosity does not depend on temperature. - -block options -name thermal_formulation -type string -shape -reader urword -optional true -valid linear nonlinear -mf6internal thermal_form -longname keyword to specify viscosity formulation for the temperature species -description may be used for specifying which viscosity formulation to use for the temperature species. Can be either LINEAR or NONLINEAR. The LINEAR viscosity formulation is the default. - -block options -name thermal_a2 -type double precision -reader urword -optional true -longname coefficient used in nonlinear viscosity function -description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A2 is not specified, a default value of 10.0 is assigned (Voss, 1984). -default_value 10. - -block options -name thermal_a3 -type double precision -reader urword -optional true -longname coefficient used in nonlinear viscosity function -description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A3 is not specified, a default value of 248.37 is assigned (Voss, 1984). -default_value 248.37 - -block options -name thermal_a4 -type double precision -reader urword -optional true -longname coefficient used in nonlinear viscosity function -description is an empirical parameter specified by the user for calculating viscosity using a nonlinear formulation. If A4 is not specified, a default value of 133.15 is assigned (Voss, 1984). -default_value 133.15 - -block options -name viscosity_filerecord -type record viscosity fileout viscosityfile -shape -reader urword -tagged true -optional true -mf6internal viscosity_fr -longname -description - -block options -name viscosity -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname viscosity keyword -description keyword to specify that record corresponds to viscosity. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name viscosityfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write viscosity information. The viscosity file has the same format as the head file. Viscosity values will be written to the viscosity file whenever heads are written to the binary head file. The settings for controlling head output are contained in the Output Control option. - - -# --------------------- gwf vsc dimensions --------------------- - -block dimensions -name nviscspecies -type integer -reader urword -optional false -longname number of species used in viscosity equation of state -description number of species used in the viscosity equation of state. If either concentrations or temperature (or both) are used to update viscosity then nviscspecies needs to be at least one. - - -# --------------------- gwf vsc packagedata --------------------- - -block packagedata -name packagedata -type recarray iviscspec dviscdc cviscref modelname auxspeciesname -shape (nviscspecies) -reader urword -longname -description - -block packagedata -name iviscspec -type integer -shape -tagged false -in_record true -reader urword -longname species number for this entry -description integer value that defines the species number associated with the specified PACKAGEDATA data entered on each line. IVISCSPECIES must be greater than zero and less than or equal to NVISCSPECIES. Information must be specified for each of the NVISCSPECIES species or the program will terminate with an error. The program will also terminate with an error if information for a species is specified more than once. -numeric_index true - -block packagedata -name dviscdc -type double precision -shape -tagged false -in_record true -reader urword -longname slope of the line that defines the linear relationship between viscosity and temperature or between viscosity and concentration, depending on the type of species entered on each line. -description real value that defines the slope of the line defining the linear relationship between viscosity and temperature or between viscosity and concentration, depending on the type of species entered on each line. If the value of AUXSPECIESNAME entered on a line corresponds to TEMPERATURE\_SPECIES\_NAME (in the OPTIONS block), this value will be used when VISCOSITY\_FUNC is equal to LINEAR (the default) in the OPTIONS block. When VISCOSITY\_FUNC is set to NONLINEAR, a value for DVISCDC must be specified though it is not used. - -block packagedata -name cviscref -type double precision -shape -tagged false -in_record true -reader urword -longname reference temperature value or reference concentration value -description real value that defines the reference temperature or reference concentration value used for this species in the viscosity equation of state. If AUXSPECIESNAME entered on a line corresponds to TEMPERATURE\_SPECIES\_NAME (in the OPTIONS block), then CVISCREF refers to a reference temperature, otherwise it refers to a reference concentration. - -block packagedata -name modelname -type string -in_record true -tagged false -shape -reader urword -longname modelname -description name of a GWT or GWE model used to simulate a species that will be used in the viscosity equation of state. This name will have no effect if the simulation does not include a GWT or GWE model that corresponds to this GWF model. - -block packagedata -name auxspeciesname -type string -in_record true -tagged false -shape -reader urword -longname auxspeciesname -description name of an auxiliary variable in a GWF stress package that will be used for this species to calculate the viscosity values. If a viscosity value is needed by the Viscosity Package then it will use the temperature or concentration values associated with this AUXSPECIESNAME in the viscosity equation of state. For advanced stress packages (LAK, SFR, MAW, and UZF) that have an associated advanced transport package (LKT, SFT, MWT, and UZT), the FLOW\_PACKAGE\_AUXILIARY\_NAME option in the advanced transport package can be used to transfer simulated temperature or concentration(s) into the flow package auxiliary variable. In this manner, the Viscosity Package can calculate viscosity values for lakes, streams, multi-aquifer wells, and unsaturated zone flow cells using simulated concentrations. - diff --git a/autotest/autotest/temp/dfn/gwf-wel.dfn b/autotest/autotest/temp/dfn/gwf-wel.dfn deleted file mode 100644 index 644c3651..00000000 --- a/autotest/autotest/temp/dfn/gwf-wel.dfn +++ /dev/null @@ -1,285 +0,0 @@ -# --------------------- gwf wel options --------------------- -# flopy multi-package -# package-type stress-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'well flow rate'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'well'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'well'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'well'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'well'} -mf6internal ipakcb - -block options -name auto_flow_reduce -type double precision -reader urword -optional true -longname cell fractional thickness for reduced pumping -description keyword and real value that defines the fraction of the cell thickness used as an interval for smoothly adjusting negative pumping rates to 0 in cells with head values less than or equal to the bottom of the cell. Negative pumping rates are adjusted to 0 or a smaller negative value when the head in the cell is equal to or less than the calculated interval above the cell bottom. AUTO\_FLOW\_REDUCE is set to 0.1 if the specified value is less than or equal to zero. By default, negative pumping rates are not reduced during a simulation. This AUTO\_FLOW\_REDUCE option only applies to wells in model cells that are marked as ``convertible'' (ICELLTYPE /= 0) in the Node Property Flow (NPF) input file. Reduction in flow will not occur for wells in cells marked as confined (ICELLTYPE = 0). -mf6internal flowred - -block options -name afrcsv_filerecord -type record auto_flow_reduce_csv fileout afrcsvfile -shape -reader urword -tagged true -optional true -longname -description -mf6internal afrcsv_rec - -block options -name auto_flow_reduce_csv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the AUTO\_FLOW\_REDUCE output option in which a new record is written for each well and for each time step in which the user-requested extraction rate is reduced by the program. -mf6internal afrcsv - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name afrcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write information about well extraction rates that have been reduced by the program. Entries are only written if the extraction rates are reduced. - -block options -name flow_reduction_length -type keyword -reader urword -optional true -longname flow reduction length keyword -description keyword that indicates the AUTO\_FLOW\_REDUCE value is a length instead of a fraction of the cell thickness. A warning will be issued if the FLOW\_REDUCTION\_LENGTH option is specified but the AUTO\_FLOW\_REDUCE option is not specified in the options block. The program will terminate with an error if the FLOW\_REDUCTION\_LENGTH option is specified and the AUTO\_FLOW\_REDUCE value specified in the options block is less than or equal to zero. -mf6internal iflowredlen - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'WEL', '{#2}': '\\ref{table:gwf-obstypetable}'} - -block options -name mover -type keyword -tagged true -reader urword -optional true -longname -description REPLACE mover {'{#1}': 'Well'} - -# --------------------- gwf wel dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of wells -description REPLACE maxbound {'{#1}': 'wells'} - - -# --------------------- gwf wel period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid q aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name q -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname well rate -description is the volumetric well rate. A positive value indicates recharge (injection) and a negative value indicates discharge (extraction). 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. - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'well'} -mf6internal auxvar - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname well name -description REPLACE boundname {'{#1}': 'well'} diff --git a/autotest/autotest/temp/dfn/gwf-welg.dfn b/autotest/autotest/temp/dfn/gwf-welg.dfn deleted file mode 100644 index f6a765a8..00000000 --- a/autotest/autotest/temp/dfn/gwf-welg.dfn +++ /dev/null @@ -1,233 +0,0 @@ -# --------------------- gwf wel options --------------------- -# flopy multi-package -# package-type stress-package - -block options -name readarraygrid -type keyword -reader urword -optional false -developmode true -longname use array-based grid input -description indicates that array-based grid input will be used for the well boundary package. This keyword must be specified to use array-based grid input. When READARRAYGRID is specified, values must be provided for every cell within a model grid, even those cells that have an IDOMAIN value less than one. Values assigned to cells with IDOMAIN values less than one are not used and have no effect on simulation results. No data cells should contain the value DNODATA (3.0E+30). -default_value true - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Flow'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'well flow rate'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'well'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'well'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'well'} -mf6internal ipakcb - -block options -name auto_flow_reduce -type double precision -reader urword -optional true -longname cell fractional thickness for reduced pumping -description keyword and real value that defines the fraction of the cell thickness used as an interval for smoothly adjusting negative pumping rates to 0 in cells with head values less than or equal to the bottom of the cell. Negative pumping rates are adjusted to 0 or a smaller negative value when the head in the cell is equal to or less than the calculated interval above the cell bottom. AUTO\_FLOW\_REDUCE is set to 0.1 if the specified value is less than or equal to zero. By default, negative pumping rates are not reduced during a simulation. This AUTO\_FLOW\_REDUCE option only applies to wells in model cells that are marked as ``convertible'' (ICELLTYPE /= 0) in the Node Property Flow (NPF) input file. Reduction in flow will not occur for wells in cells marked as confined (ICELLTYPE = 0). -mf6internal flowred - -block options -name afrcsv_filerecord -type record auto_flow_reduce_csv fileout afrcsvfile -shape -reader urword -tagged true -optional true -longname -description -mf6internal afrcsv_rec - -block options -name auto_flow_reduce_csv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the AUTO\_FLOW\_REDUCE output option in which a new record is written for each well and for each time step in which the user-requested extraction rate is reduced by the program. -mf6internal afrcsv - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name afrcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write information about well extraction rates that have been reduced by the program. Entries are only written if the extraction rates are reduced. - -block options -name flow_reduction_length -type keyword -reader urword -optional true -longname flow reduction length keyword -description keyword that indicates the AUTO\_FLOW\_REDUCE value is a length instead of a fraction of the cell thickness. A warning will be issued if the FLOW\_REDUCTION\_LENGTH option is specified but the AUTO\_FLOW\_REDUCE option is not specified in the options block. The program will terminate with an error if the FLOW\_REDUCTION\_LENGTH option is specified and the AUTO\_FLOW\_REDUCE value specified in the options block is less than or equal to zero. -mf6internal iflowredlen - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'WEL', '{#2}': '\\ref{table:gwf-obstypetable}'} - -block options -name mover -type keyword -tagged true -reader urword -optional true -longname -description REPLACE mover {'{#1}': 'Well'} - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwf wel dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional true -longname maximum number of wells in any stress period -description REPLACE maxbound {'{#1}': 'wells'} - - -# --------------------- gwf wel period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name q -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname well rate -description is the volumetric well rate. A positive value indicates recharge (injection) and a negative value indicates discharge (extraction). -default_value 3.e30 - -block period -name aux -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname well auxiliary variable iaux -description is an array of values for auxiliary variable aux(iaux), where iaux is a value from 1 to naux, and aux(iaux) must be listed as part of the auxiliary variables. A separate array can be specified for each auxiliary variable. If the value specified here for the auxiliary variable is the same as auxmultname, then the well rate array will be multiplied by this array. -mf6internal auxvar diff --git a/autotest/autotest/temp/dfn/gwt-adv.dfn b/autotest/autotest/temp/dfn/gwt-adv.dfn deleted file mode 100644 index 519f0bc6..00000000 --- a/autotest/autotest/temp/dfn/gwt-adv.dfn +++ /dev/null @@ -1,18 +0,0 @@ -# --------------------- gwt adv options --------------------- - -block options -name scheme -type string -valid central upstream tvd utvd -reader urword -optional true -longname advective scheme -description scheme used to solve the advection term. Can be upstream, central, TVD or UTVD. If not specified, upstream weighting is the default weighting scheme. - -block options -name ats_percel -type double precision -reader urword -optional true -longname fractional cell distance used for time step calculation -description fractional cell distance submitted by the ADV Package to the adaptive time stepping (ATS) package. If ATS\_PERCEL is specified and the ATS Package is active, a time step calculation will be made for each cell based on flow through the cell and cell properties. The largest time step will be calculated such that the advective fractional cell distance (ATS\_PERCEL) is not exceeded for any active cell in the grid. This time-step constraint will be submitted to the ATS Package, perhaps with constraints submitted by other packages, in the calculation of the time step. ATS\_PERCEL must be greater than zero. If a value of zero is specified for ATS\_PERCEL the program will automatically reset it to an internal no data value to indicate that time steps should not be subject to this constraint. \ No newline at end of file diff --git a/autotest/autotest/temp/dfn/gwt-api.dfn b/autotest/autotest/temp/dfn/gwt-api.dfn deleted file mode 100644 index 45b770de..00000000 --- a/autotest/autotest/temp/dfn/gwt-api.dfn +++ /dev/null @@ -1,100 +0,0 @@ -# --------------------- gwt api options --------------------- -# flopy multi-package - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'api boundary'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'api boundary'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'api boundary'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save api flows to budget file -description REPLACE save_flows {'{#1}': 'api boundary'} -mf6internal ipakcb - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'api boundary', '{#2}': '\\ref{table:gwt-obstypetable}'} - -block options -name mover -type keyword -tagged true -reader urword -optional true -longname -description REPLACE mover {'{#1}': 'api boundary'} - -# --------------------- gwt api dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of user-defined api boundaries -description REPLACE maxbound {'{#1}': 'api boundary'} diff --git a/autotest/autotest/temp/dfn/gwt-cnc.dfn b/autotest/autotest/temp/dfn/gwt-cnc.dfn deleted file mode 100644 index debee8e9..00000000 --- a/autotest/autotest/temp/dfn/gwt-cnc.dfn +++ /dev/null @@ -1,213 +0,0 @@ -# --------------------- gwt cnc options --------------------- -# flopy multi-package - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Transport'} - -block options -name auxmultname -type string -shape -reader urword -optional true -longname name of auxiliary variable for multiplier -description REPLACE auxmultname {'{#1}': 'concentration value'} - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'constant concentration'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'constant concentration'} -mf6internal iprpak - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'constant concentration'} -mf6internal iprflow - -block options -name save_flows -type keyword -reader urword -optional true -longname save constant concentration flows to budget file -description REPLACE save_flows {'{#1}': 'constant concentration'} -mf6internal ipakcb - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname time series keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'CNC', '{#2}': '\\ref{table:gwt-obstypetable}'} - - -# --------------------- gwt cnc dimensions --------------------- - -block dimensions -name maxbound -type integer -reader urword -optional false -longname maximum number of constant concentrations -description REPLACE maxbound {'{#1}': 'constant concentrations'} - - -# --------------------- gwt cnc period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name stress_period_data -type recarray cellid conc aux boundname -shape (maxbound) -reader urword -longname -description -mf6internal spd - -block period -name cellid -type integer -shape (ncelldim) -tagged false -in_record true -reader urword -longname cell identifier -description REPLACE cellid {} - -block period -name conc -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname constant concentration value -description is the constant concentration value. 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. -mf6internal tspvar - -block period -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -optional true -time_series true -longname auxiliary variables -description REPLACE aux {'{#1}': 'constant concentration'} -mf6internal auxvar - -block period -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname constant concentration name -description REPLACE boundname {'{#1}': 'constant concentration'} diff --git a/autotest/autotest/temp/dfn/gwt-dis.dfn b/autotest/autotest/temp/dfn/gwt-dis.dfn deleted file mode 100644 index 7be72617..00000000 --- a/autotest/autotest/temp/dfn/gwt-dis.dfn +++ /dev/null @@ -1,239 +0,0 @@ -# --------------------- gwt dis options --------------------- -# mf6 subpackage utl-ncf - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position of the model grid origin -description x-position of the lower-left corner of the model grid. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position of the model grid origin -description y-position of the lower-left corner of the model grid. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the lower-left corner of the model grid. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name ncf_filerecord -type record ncf6 filein ncf6_filename -reader urword -tagged true -optional true -longname -description - -block options -name ncf6 -type keyword -in_record true -reader urword -tagged true -optional false -longname ncf keyword -description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ncf6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of NCF information -description defines a NetCDF configuration (NCF) input file. -extended true - -# --------------------- gwt dis dimensions --------------------- - -block dimensions -name nlay -type integer -reader urword -optional false -longname number of layers -description is the number of layers in the model grid. -default_value 1 - -block dimensions -name nrow -type integer -reader urword -optional false -longname number of rows -description is the number of rows in the model grid. -default_value 2 - -block dimensions -name ncol -type integer -reader urword -optional false -longname number of columns -description is the number of columns in the model grid. -default_value 2 - -# --------------------- gwt dis griddata --------------------- - -block griddata -name delr -type double precision -shape (ncol) -reader readarray -netcdf true -longname spacing along a row -description is the column spacing in the row direction. -default_value 1.0 - -block griddata -name delc -type double precision -shape (nrow) -reader readarray -netcdf true -longname spacing along a column -description is the row spacing in the column direction. -default_value 1.0 - -block griddata -name top -type double precision -shape (ncol, nrow) -reader readarray -netcdf true -longname cell top elevation -description is the top elevation for each cell in the top model layer. -default_value 1.0 - -block griddata -name botm -type double precision -shape (ncol, nrow, nlay) -reader readarray -netcdf true -layered true -longname cell bottom elevation -description is the bottom elevation for each cell. -default_value 0. - -block griddata -name idomain -type integer -shape (ncol, nrow, nlay) -reader readarray -layered true -netcdf true -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. - - diff --git a/autotest/autotest/temp/dfn/gwt-disu.dfn b/autotest/autotest/temp/dfn/gwt-disu.dfn deleted file mode 100644 index eacf163b..00000000 --- a/autotest/autotest/temp/dfn/gwt-disu.dfn +++ /dev/null @@ -1,337 +0,0 @@ -# --------------------- gwt disu options --------------------- - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position origin of the model grid coordinate system -description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position origin of the model grid coordinate system -description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name vertical_offset_tolerance -type double precision -reader urword -optional true -default_value 0.0 -longname vertical length dimension for top and bottom checking -description checks are performed to ensure that the top of a cell is not higher than the bottom of an overlying cell. This option can be used to specify the tolerance that is used for checking. If top of a cell is above the bottom of an overlying cell by a value less than this tolerance, then the program will not terminate with an error. The default value is zero. This option should generally not be used. -mf6internal voffsettol - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -# --------------------- gwt disu dimensions --------------------- - -block dimensions -name nodes -type integer -reader urword -optional false -longname number of layers -description is the number of cells in the model grid. - -block dimensions -name nja -type integer -reader urword -optional false -longname number of columns -description is the sum of the number of connections and NODES. When calculating the total number of connections, the connection between cell n and cell m is considered to be different from the connection between cell m and cell n. Thus, NJA is equal to the total number of connections, including n to m and m to n, and the total number of cells. - -block dimensions -name nvert -type integer -reader urword -optional true -longname number of vertices -description is the total number of (x, y) vertex pairs used to define the plan-view shape of each cell in the model grid. If NVERT is not specified or is specified as zero, then the VERTICES and CELL2D blocks below are not read. NVERT and the accompanying VERTICES and CELL2D blocks should be specified for most simulations. If the XT3D or SAVE\_SPECIFIC\_DISCHARGE options are specified in the NPF Package, then this information is required. - -# --------------------- gwt disu griddata --------------------- - -block griddata -name top -type double precision -shape (nodes) -reader readarray -longname cell top elevation -description is the top elevation for each cell in the model grid. - -block griddata -name bot -type double precision -shape (nodes) -reader readarray -longname cell bottom elevation -description is the bottom elevation for each cell. - -block griddata -name area -type double precision -shape (nodes) -reader readarray -longname cell surface area -description is the cell surface area (in plan view). - -block griddata -name idomain -type integer -shape (nodes) -reader readarray -layered false -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1 or greater, the cell exists in the simulation. IDOMAIN values of -1 cannot be specified for the DISU Package. - -# --------------------- gwt disu connectiondata --------------------- - -block connectiondata -name iac -type integer -shape (nodes) -reader readarray -longname number of cell connections -description is the number of connections (plus 1) for each cell. The sum of all the entries in IAC must be equal to NJA. - -block connectiondata -name ja -type integer -shape (nja) -reader readarray -longname grid connectivity -description is a list of cell number (n) followed by its connecting cell numbers (m) for each of the m cells connected to cell n. The number of values to provide for cell n is IAC(n). This list is sequentially provided for the first to the last cell. The first value in the list must be cell n itself, and the remaining cells must be listed in an increasing order (sorted from lowest number to highest). Note that the cell and its connections are only supplied for the GWT cells and their connections to the other GWT cells. Also note that the JA list input may be divided such that every node and its connectivity list can be on a separate line for ease in readability of the file. To further ease readability of the file, the node number of the cell whose connectivity is subsequently listed, may be expressed as a negative number, the sign of which is subsequently converted to positive by the code. -numeric_index true -jagged_array iac - -block connectiondata -name ihc -type integer -shape (nja) -reader readarray -longname connection type -description is an index array indicating the direction between node n and all of its m connections. If IHC = 0 then cell n and cell m are connected in the vertical direction. Cell n overlies cell m if the cell number for n is less than m; cell m overlies cell n if the cell number for m is less than n. If IHC = 1 then cell n and cell m are connected in the horizontal direction. If IHC = 2 then cell n and cell m are connected in the horizontal direction, and the connection is vertically staggered. A vertically staggered connection is one in which a cell is horizontally connected to more than one cell in a horizontal connection. -jagged_array iac - -block connectiondata -name cl12 -type double precision -shape (nja) -reader readarray -longname connection lengths -description is the array containing connection lengths between the center of cell n and the shared face with each adjacent m cell. -jagged_array iac - -block connectiondata -name hwva -type double precision -shape (nja) -reader readarray -longname connection lengths -description is a symmetric array of size NJA. For horizontal connections, entries in HWVA are the horizontal width perpendicular to flow. For vertical connections, entries in HWVA are the vertical area for flow. Thus, values in the HWVA array contain dimensions of both length and area. Entries in the HWVA array have a one-to-one correspondence with the connections specified in the JA array. Likewise, there is a one-to-one correspondence between entries in the HWVA array and entries in the IHC array, which specifies the connection type (horizontal or vertical). Entries in the HWVA array must be symmetric; the program will terminate with an error if the value for HWVA for an n to m connection does not equal the value for HWVA for the corresponding n to m connection. -jagged_array iac - -block connectiondata -name angldegx -type double precision -optional true -shape (nja) -reader readarray -longname angle of face normal to connection -description is the angle (in degrees) between the horizontal x-axis and the outward normal to the face between a cell and its connecting cells. The angle varies between zero and 360.0 degrees, where zero degrees points in the positive x-axis direction, and 90 degrees points in the positive y-axis direction. ANGLDEGX is only needed if horizontal anisotropy is specified in the NPF Package, if the XT3D option is used in the NPF Package, or if the SAVE\_SPECIFIC\_DISCHARGE option is specified in the NPF Package. ANGLDEGX does not need to be specified if these conditions are not met. ANGLDEGX is of size NJA; values specified for vertical connections and for the diagonal position are not used. Note that ANGLDEGX is read in degrees, which is different from MODFLOW-USG, which reads a similar variable (ANGLEX) in radians. -jagged_array iac - -# --------------------- gwt disu vertices --------------------- - -block vertices -name vertices -type recarray iv xv yv -shape (nvert) -reader urword -optional true -longname vertices data -description - -block vertices -name iv -type integer -in_record true -tagged false -reader urword -optional false -longname vertex number -description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. -numeric_index true - -block vertices -name xv -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for vertex -description is the x-coordinate for the vertex. - -block vertices -name yv -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for vertex -description is the y-coordinate for the vertex. - - -# --------------------- gwt disu cell2d --------------------- - -block cell2d -name cell2d -type recarray icell2d xc yc ncvert icvert -shape (nodes) -reader urword -optional true -longname cell2d data -description - -block cell2d -name icell2d -type integer -in_record true -tagged false -reader urword -optional false -longname cell2d number -description is the cell2d number. Records in the CELL2D block must be listed in consecutive order from 1 to NODES. -numeric_index true - -block cell2d -name xc -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for cell center -description is the x-coordinate for the cell center. - -block cell2d -name yc -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for cell center -description is the y-coordinate for the cell center. - -block cell2d -name ncvert -type integer -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. - -block cell2d -name icvert -type integer -shape (ncvert) -in_record true -tagged false -reader urword -optional false -longname array of vertex numbers -description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. -numeric_index true diff --git a/autotest/autotest/temp/dfn/gwt-disv.dfn b/autotest/autotest/temp/dfn/gwt-disv.dfn deleted file mode 100644 index 29ad0e5a..00000000 --- a/autotest/autotest/temp/dfn/gwt-disv.dfn +++ /dev/null @@ -1,320 +0,0 @@ -# --------------------- gwt disv options --------------------- -# mf6 subpackage utl-ncf - -block options -name length_units -type string -reader urword -optional true -longname model length units -description is the length units used for this model. Values can be ``FEET'', ``METERS'', or ``CENTIMETERS''. If not specified, the default is ``UNKNOWN''. - -block options -name nogrb -type keyword -reader urword -optional true -longname do not write binary grid file -description keyword to deactivate writing of the binary grid file. - -block options -name grb_filerecord -type record grb6 fileout grb6_filename -reader urword -tagged true -optional true -longname -description - -block options -name grb6 -type keyword -in_record true -reader urword -tagged true -optional false -longname grb keyword -description keyword to specify that record corresponds to a binary grid file. - -block options -name fileout -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name grb6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of GRB information -description defines a binary grid output file. If this option is not provided, the output file will have the same name as the discretization input file, plus extension ``.grb''. - -block options -name xorigin -type double precision -reader urword -optional true -longname x-position origin of the model grid coordinate system -description x-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. A default value of zero is assigned if not specified. The value for XORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name yorigin -type double precision -reader urword -optional true -longname y-position origin of the model grid coordinate system -description y-position of the origin used for model grid vertices. This value should be provided in a real-world coordinate system. If not specified, then a default value equal to zero is used. The value for YORIGIN does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name angrot -type double precision -reader urword -optional true -longname rotation angle -description counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system. If not specified, then a default value of 0.0 is assigned. The value for ANGROT does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -block options -name crs -type string -shape lenbigline -preserve_case true -reader urword -optional true -developmode true -longname CRS user input string -description is a real-world coordinate reference system (CRS) for the model, for example, an EPSG integer code (e.g. 26915), authority string (i.e. epsg:26915), or Open Geospatial Consortium Well-Known Text (WKT) specification. Limited to 5000 characters. The entry for CRS does not affect the model simulation, but it is written to the binary grid file so that postprocessors can locate the grid in space. - -block options -name ncf_filerecord -type record ncf6 filein ncf6_filename -reader urword -tagged true -optional true -longname -description - -block options -name ncf6 -type keyword -in_record true -reader urword -tagged true -optional false -longname ncf keyword -description keyword to specify that record corresponds to a NetCDF configuration (NCF) file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ncf6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of NCF information -description defines a NetCDF configuration (NCF) input file. -extended true - -# --------------------- gwt disv dimensions --------------------- - -block dimensions -name nlay -type integer -reader urword -optional false -longname number of layers -description is the number of layers in the model grid. - -block dimensions -name ncpl -type integer -reader urword -optional false -longname number of cells per layer -description is the number of cells per layer. This is a constant value for the grid and it applies to all layers. - -block dimensions -name nvert -type integer -reader urword -optional false -longname number of columns -description is the total number of (x, y) vertex pairs used to characterize the horizontal configuration of the model grid. - -# --------------------- gwt disv griddata --------------------- - -block griddata -name top -type double precision -shape (ncpl) -reader readarray -netcdf true -longname model top elevation -description is the top elevation for each cell in the top model layer. - -block griddata -name botm -type double precision -shape (ncpl, nlay) -reader readarray -layered true -netcdf true -longname model bottom elevation -description is the bottom elevation for each cell. - -block griddata -name idomain -type integer -shape (ncpl, nlay) -reader readarray -layered true -netcdf true -optional true -longname idomain existence array -description is an optional array that characterizes the existence status of a cell. If the IDOMAIN array is not specified, then all model cells exist within the solution. If the IDOMAIN value for a cell is 0, the cell does not exist in the simulation. Input and output values will be read and written for the cell, but internal to the program, the cell is excluded from the solution. If the IDOMAIN value for a cell is 1, the cell exists in the simulation. If the IDOMAIN value for a cell is -1, the cell does not exist in the simulation. Furthermore, the first existing cell above will be connected to the first existing cell below. This type of cell is referred to as a ``vertical pass through'' cell. - - -# --------------------- gwt disv vertices --------------------- - -block vertices -name vertices -type recarray iv xv yv -shape (nvert) -reader urword -optional false -longname vertices data -description - -block vertices -name iv -type integer -in_record true -tagged false -reader urword -optional false -longname vertex number -description is the vertex number. Records in the VERTICES block must be listed in consecutive order from 1 to NVERT. -numeric_index true - -block vertices -name xv -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for vertex -description is the x-coordinate for the vertex. - -block vertices -name yv -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for vertex -description is the y-coordinate for the vertex. - - -# --------------------- gwt disv cell2d --------------------- - -block cell2d -name cell2d -type recarray icell2d xc yc ncvert icvert -shape (ncpl) -reader urword -optional false -longname cell2d data -description - -block cell2d -name icell2d -type integer -in_record true -tagged false -reader urword -optional false -longname cell2d number -description is the CELL2D number. Records in the CELL2D block must be listed in consecutive order from the first to the last. -numeric_index true - -block cell2d -name xc -type double precision -in_record true -tagged false -reader urword -optional false -longname x-coordinate for cell center -description is the x-coordinate for the cell center. - -block cell2d -name yc -type double precision -in_record true -tagged false -reader urword -optional false -longname y-coordinate for cell center -description is the y-coordinate for the cell center. - -block cell2d -name ncvert -type integer -in_record true -tagged false -reader urword -optional false -longname number of cell vertices -description is the number of vertices required to define the cell. There may be a different number of vertices for each cell. - -block cell2d -name icvert -type integer -shape (ncvert) -in_record true -tagged false -reader urword -optional false -longname array of vertex numbers -description is an array of integer values containing vertex numbers (in the VERTICES block) used to define the cell. Vertices must be listed in clockwise order. Cells that are connected must share vertices. -numeric_index true diff --git a/autotest/autotest/temp/dfn/gwt-dsp.dfn b/autotest/autotest/temp/dfn/gwt-dsp.dfn deleted file mode 100644 index d8b5f590..00000000 --- a/autotest/autotest/temp/dfn/gwt-dsp.dfn +++ /dev/null @@ -1,106 +0,0 @@ -# --------------------- gwt dsp options --------------------- - -block options -name xt3d_off -type keyword -shape -reader urword -optional true -longname deactivate xt3d -description deactivate the xt3d method and use the faster and less accurate approximation. This option may provide a fast and accurate solution under some circumstances, such as when flow aligns with the model grid, there is no mechanical dispersion, or when the longitudinal and transverse dispersivities are equal. This option may also be used to assess the computational demand of the XT3D approach by noting the run time differences with and without this option on. - -block options -name xt3d_rhs -type keyword -shape -reader urword -optional true -longname xt3d on right-hand side -description add xt3d terms to right-hand side, when possible. This option uses less memory, but may require more iterations. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwt dsp griddata --------------------- - -block griddata -name diffc -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname effective molecular diffusion coefficient -description effective molecular diffusion coefficient. - -block griddata -name alh -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname longitudinal dispersivity in horizontal direction -description longitudinal dispersivity in horizontal direction. If flow is strictly horizontal, then this is the longitudinal dispersivity that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. - -block griddata -name alv -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname longitudinal dispersivity in vertical direction -description longitudinal dispersivity in vertical direction. If flow is strictly vertical, then this is the longitudinal dispsersivity value that will be used. If flow is not strictly horizontal or strictly vertical, then the longitudinal dispersivity is a function of both ALH and ALV. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ALH. - -block griddata -name ath1 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity in horizontal direction -description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the second ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the y direction. If mechanical dispersion is represented (by specifying any dispersivity values) then this array is required. - -block griddata -name ath2 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity in horizontal direction -description transverse dispersivity in horizontal direction. This is the transverse dispersivity value for the third ellipsoid axis. If flow is strictly horizontal and directed in the x direction (along a row for a regular grid), then this value controls spreading in the z direction. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH1. - -block griddata -name atv -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname transverse dispersivity when flow is in vertical direction -description transverse dispersivity when flow is in vertical direction. If flow is strictly vertical and directed in the z direction, then this value controls spreading in the x and y directions. If this value is not specified and mechanical dispersion is represented, then this array is set equal to ATH2. diff --git a/autotest/autotest/temp/dfn/gwt-fmi.dfn b/autotest/autotest/temp/dfn/gwt-fmi.dfn deleted file mode 100644 index f7402caf..00000000 --- a/autotest/autotest/temp/dfn/gwt-fmi.dfn +++ /dev/null @@ -1,59 +0,0 @@ -# --------------------- gwt fmi options --------------------- - -block options -name save_flows -type keyword -reader urword -optional true -longname save calculated flow imbalance correction to budget file -description REPLACE save_flows {'{#1}': 'FMI'} - -block options -name flow_imbalance_correction -type keyword -reader urword -optional true -mf6internal imbalancecorrect -longname correct for flow imbalance -description correct for an imbalance in flows by assuming that any residual flow error comes in or leaves at the concentration of the cell. When this option is activated, the GWT Model budget written to the listing file will contain two additional entries: FLOW-ERROR and FLOW-CORRECTION. These two entries will be equal but opposite in sign. The FLOW-CORRECTION term is a mass flow that is added to offset the error caused by an imprecise flow balance. If these terms are not relatively small, the flow model should be rerun with stricter convergence tolerances. - -# --------------------- gwt fmi packagedata --------------------- - -block packagedata -name packagedata -type recarray flowtype filein fname -reader urword -optional true -longname flowtype list -description - -block packagedata -name flowtype -in_record true -type string -tagged false -reader urword -longname flow type -description is the word GWFBUDGET, GWFHEAD, GWFMOVER or the name of an advanced GWF stress package. If GWFBUDGET is specified, then the corresponding file must be a budget file. If GWFHEAD is specified, the file must be a head file. If GWFGRID is specified, the file must be a binary grid file. If an advanced GWF stress package name appears then the corresponding file must be the budget file saved by a LAK, SFR, MAW or UZF Package. - -block packagedata -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block packagedata -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing flows. The path to the file should be included if the file is not located in the folder where the program was run. - diff --git a/autotest/autotest/temp/dfn/gwt-ic.dfn b/autotest/autotest/temp/dfn/gwt-ic.dfn deleted file mode 100644 index f96cb7da..00000000 --- a/autotest/autotest/temp/dfn/gwt-ic.dfn +++ /dev/null @@ -1,33 +0,0 @@ -# --------------------- gwt ic options --------------------- - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwt ic griddata --------------------- - -block griddata -name strt -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname starting concentration -description is the initial (starting) concentration---that is, concentration at the beginning of the GWT Model simulation. STRT must be specified for all GWT Model simulations. One value is read for every model cell. -default_value 0.0 diff --git a/autotest/autotest/temp/dfn/gwt-ist.dfn b/autotest/autotest/temp/dfn/gwt-ist.dfn deleted file mode 100644 index 287421ce..00000000 --- a/autotest/autotest/temp/dfn/gwt-ist.dfn +++ /dev/null @@ -1,377 +0,0 @@ -# --------------------- gwt ist options --------------------- -# flopy multi-package - -block options -name save_flows -type keyword -reader urword -optional true -longname save calculated flows to budget file -description REPLACE save_flows {'{#1}': 'IST'} - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -mf6internal budfilerec -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -mf6internal budcsvfilerec -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name sorption -type string -valid linear freundlich langmuir -reader urword -optional true -longname activate sorption -description is a text keyword to indicate that sorption will be activated. Valid sorption options include LINEAR, FREUNDLICH, and LANGMUIR. Use of this keyword requires that BULK\_DENSITY and DISTCOEF are specified in the GRIDDATA block. If sorption is specified as FREUNDLICH or LANGMUIR then SP2 is also required in the GRIDDATA block. The sorption option must be consistent with the sorption option specified in the MST Package or the program will terminate with an error. - -block options -name first_order_decay -type keyword -reader urword -optional true -longname activate first-order decay -mf6internal order1_decay -description is a text keyword to indicate that first-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. - -block options -name zero_order_decay -type keyword -reader urword -optional true -mf6internal order0_decay -longname activate zero-order decay -description is a text keyword to indicate that zero-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. - -block options -name cim_filerecord -type record cim fileout cimfile -shape -reader urword -tagged true -optional true -mf6internal cimfilerec -longname -description - -block options -name cim -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname cim keyword -mf6internal cimopt -description keyword to specify that record corresponds to immobile concentration. - -block options -name cimfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write immobile concentrations. This file is a binary file that has the same format and structure as a binary head and concentration file. The value for the text variable written to the file is CIM. Immobile domain concentrations will be written to this file at the same interval as mobile domain concentrations are saved, as specified in the GWT Model Output Control file. - -block options -name cimprintrecord -type record cim print_format formatrecord -shape -reader urword -optional true -longname -description - -block options -name print_format -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to indicate that a print format follows -description keyword to specify format for printing to the listing file. - -block options -name formatrecord -type record columns width digits format -shape -in_record true -reader urword -tagged -optional false -longname -description - -block options -name columns -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of columns -description number of columns for writing data. - -block options -name width -type integer -shape -in_record true -reader urword -tagged true -optional -longname width for each number -description width for writing each number. - -block options -name digits -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of digits -description number of digits to use for writing a number. - -block options -name format -type string -shape -in_record true -reader urword -tagged false -optional false -longname write format -description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. - -block options -name sorbate_filerecord -type record sorbate fileout sorbatefile -shape -reader urword -tagged true -optional true -mf6internal sorbatefilerec -longname -description - -block options -name sorbate -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname sorbate keyword -description keyword to specify that record corresponds to immobile sorbate concentration. - -block options -name sorbatefile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write immobile sorbate concentration information. Immobile sorbate concentrations will be written whenever aqueous immobile concentrations are saved, as determined by settings in the Output Control option. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwt ist griddata --------------------- - -block griddata -name porosity -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname porosity of the immobile domain -description porosity of the immobile domain specified as the immobile domain pore volume per immobile domain volume. - -block griddata -name volfrac -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname volume fraction of this immobile domain -description fraction of the cell volume that consists of this immobile domain. The sum of all immobile domain volume fractions must be less than one. - -block griddata -name zetaim -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname mass transfer rate coefficient between the mobile and immobile domains -description mass transfer rate coefficient between the mobile and immobile domains, in dimensions of per time. - -block griddata -name cim -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname initial concentration of the immobile domain -description initial concentration of the immobile domain in mass per length cubed. If CIM is not specified, then it is assumed to be zero. - -block griddata -name decay -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname first rate coefficient -description is the rate coefficient for first or zero-order decay for the aqueous phase of the immobile domain. A negative value indicates solute production. The dimensions of decay for first-order decay is one over time. The dimensions of decay for zero-order decay is mass per length cubed per time. Decay will have no effect on simulation results unless either first- or zero-order decay is specified in the options block. - -block griddata -name decay_sorbed -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname second rate coefficient -description is the rate coefficient for first or zero-order decay for the sorbed phase of the immobile domain. A negative value indicates solute production. The dimensions of decay\_sorbed for first-order decay is one over time. The dimensions of decay\_sorbed for zero-order decay is mass of solute per mass of aquifer per time. If decay\_sorbed is not specified and both decay and sorption are active, then the program will terminate with an error. decay\_sorbed will have no effect on simulation results unless the SORPTION keyword and either first- or zero-order decay are specified in the options block. - -block griddata -name bulk_density -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname bulk density -description is the bulk density of this immobile domain in mass per length cubed. Bulk density is defined as the immobile domain solid mass per volume of the immobile domain. bulk\_density is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, bulk\_density will have no effect on simulation results. - -block griddata -name distcoef -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname distribution coefficient -description is the distribution coefficient for the equilibrium-controlled linear sorption isotherm in dimensions of length cubed per mass. distcoef is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, distcoef will have no effect on simulation results. - -block griddata -name sp2 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname second sorption parameter -description is the exponent for the Freundlich isotherm and the sorption capacity for the Langmuir isotherm. sp2 is not required unless the SORPTION keyword is specified in the options block and sorption is specified as FREUNDLICH or LANGMUIR. If the SORPTION keyword is not specified in the options block, or if sorption is specified as LINEAR, sp2 will have no effect on simulation results. diff --git a/autotest/autotest/temp/dfn/gwt-lkt.dfn b/autotest/autotest/temp/dfn/gwt-lkt.dfn deleted file mode 100644 index f32d282d..00000000 --- a/autotest/autotest/temp/dfn/gwt-lkt.dfn +++ /dev/null @@ -1,460 +0,0 @@ -# --------------------- gwt lkt options --------------------- -# flopy multi-package - -block options -name flow_package_name -type string -shape -reader urword -optional true -longname keyword to specify name of corresponding flow package -description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWT name file). - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Transport'} - -block options -name flow_package_auxiliary_name -type string -shape -reader urword -optional true -longname keyword to specify name of concentration auxiliary variable in flow package -description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated concentrations from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'lake'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'lake'} - -block options -name print_concentration -type keyword -reader urword -optional true -longname print calculated stages to listing file -description REPLACE print_concentration {'{#1}': 'lake', '{#2}': 'concentration', '{#3}': 'CONCENTRATION'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'lake'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save lake flows to budget file -description REPLACE save_flows {'{#1}': 'lake'} - -block options -name concentration_filerecord -type record concentration fileout concfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name concentration -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname stage keyword -description keyword to specify that record corresponds to concentration. - -block options -name concfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write concentration information. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'LKT', '{#2}': '\\ref{table:gwt-obstypetable}'} - - -# --------------------- gwt lkt packagedata --------------------- - -block packagedata -name packagedata -type recarray ifno strt aux boundname -shape (maxbound) -reader urword -longname -description - -block packagedata -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname lake number for this entry -description integer value that defines the feature (lake) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NLAKES. Lake information must be specified for every lake or the program will terminate with an error. The program will also terminate with an error if information for a lake is specified more than once. -numeric_index true - -block packagedata -name strt -type double precision -shape -tagged false -in_record true -reader urword -longname starting lake concentration -description real value that defines the starting concentration for the lake. - -block packagedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -time_series true -optional true -longname auxiliary variables -description REPLACE aux {'{#1}': 'lake'} - -block packagedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname lake name -description REPLACE boundname {'{#1}': 'lake'} - - -# --------------------- gwt lkt period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name lakeperioddata -type recarray ifno laksetting -shape -reader urword -longname -description - -block period -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname lake number for this entry -description integer value that defines the feature (lake) number associated with the specified PERIOD data on the line. IFNO must be greater than zero and less than or equal to NLAKES. -numeric_index true - -block period -name laksetting -type keystring status concentration rainfall evaporation runoff ext-inflow auxiliaryrecord -shape -tagged false -in_record true -reader urword -longname -description line of information that is parsed into a keyword and values. Keyword values that can be used to start the LAKSETTING string include: STATUS, CONCENTRATION, RAINFALL, EVAPORATION, RUNOFF, EXT-INFLOW, and AUXILIARY. These settings are used to assign the concentration of associated with the corresponding flow terms. Concentrations cannot be specified for all flow terms. For example, the Lake Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the lake at the calculated concentration of the lake. - -block period -name status -type string -shape -tagged true -in_record true -reader urword -longname lake concentration status -description keyword option to define lake status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that concentration will be calculated for the lake. If a lake is inactive, then there will be no solute mass fluxes into or out of the lake and the inactive value will be written for the lake concentration. If a lake is constant, then the concentration for the lake will be fixed at the user specified value. - -block period -name concentration -type string -shape -tagged true -in_record true -time_series true -reader urword -longname lake concentration -description real or character value that defines the concentration for the lake. The specified CONCENTRATION is only applied if the lake is a constant concentration lake. 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. - -block period -name rainfall -type string -shape -tagged true -in_record true -reader urword -time_series true -longname rainfall concentration -description real or character value that defines the rainfall solute concentration $(ML^{-3})$ for the lake. 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. - -block period -name evaporation -type string -shape -tagged true -in_record true -reader urword -time_series true -longname evaporation concentration -description real or character value that defines the concentration of evaporated water $(ML^{-3})$ for the lake. If this concentration value is larger than the simulated concentration in the lake, then the evaporated water will be removed at the same concentration as the lake. 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. - -block period -name runoff -type string -shape -tagged true -in_record true -reader urword -time_series true -longname runoff concentration -description real or character value that defines the concentration of runoff $(ML^{-3})$ for the lake. Value must be greater than or equal to zero. 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. - -block period -name ext-inflow -type string -shape -tagged true -in_record true -reader urword -time_series true -longname ext-inflow concentration -description real or character value that defines the concentration of external inflow $(ML^{-3})$ for the lake. Value must be greater than or equal to zero. 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. - -block period -name auxiliaryrecord -type record auxiliary auxname auxval -shape -tagged -in_record true -reader urword -longname -description - -block period -name auxiliary -type keyword -shape -in_record true -reader urword -longname -description keyword for specifying auxiliary variable. - -block period -name auxname -type string -shape -tagged false -in_record true -reader urword -longname -description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. - -block period -name auxval -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname auxiliary variable value -description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwt-mst.dfn b/autotest/autotest/temp/dfn/gwt-mst.dfn deleted file mode 100644 index 8e97da15..00000000 --- a/autotest/autotest/temp/dfn/gwt-mst.dfn +++ /dev/null @@ -1,168 +0,0 @@ -# --------------------- gwt mst options --------------------- - -block options -name save_flows -type keyword -reader urword -optional true -longname save calculated flows to budget file -description REPLACE save_flows {'{#1}': 'MST'} - -block options -name first_order_decay -type keyword -reader urword -optional true -mf6internal order1_decay -longname activate first-order decay -description is a text keyword to indicate that first-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. - -block options -name zero_order_decay -type keyword -reader urword -optional true -mf6internal order0_decay -longname activate zero-order decay -description is a text keyword to indicate that zero-order decay will occur. Use of this keyword requires that DECAY and DECAY\_SORBED (if sorption is active) are specified in the GRIDDATA block. - -block options -name sorption -type string -valid linear freundlich langmuir -reader urword -optional true -longname activate sorption -description is a text keyword to indicate that sorption will be activated. Valid sorption options include LINEAR, FREUNDLICH, and LANGMUIR. Use of this keyword requires that BULK\_DENSITY and DISTCOEF are specified in the GRIDDATA block. If sorption is specified as FREUNDLICH or LANGMUIR then SP2 is also required in the GRIDDATA block. - -block options -name sorbate_filerecord -type record sorbate fileout sorbatefile -shape -reader urword -tagged true -optional true -longname -description -mf6internal sorbate_rec - -block options -name sorbate -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname sorbate keyword -description keyword to specify that record corresponds to sorbate concentration. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name sorbatefile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write sorbate concentration information. Sorbate concentrations will be written whenever aqueous concentrations are saved, as determined by settings in the Output Control option. - -block options -name export_array_ascii -type keyword -reader urword -optional true -mf6internal export_ascii -longname export array variables to layered ascii files. -description keyword that specifies input griddata arrays should be written to layered ascii output files. - -block options -name export_array_netcdf -type keyword -reader urword -optional true -mf6internal export_nc -longname export array variables to netcdf output files. -description keyword that specifies input gridded arrays should be written to the model output NetCDF file with attributes that support using the generated file as a MODFLOW 6 simulation input. This option only has an effect when an output model NetCDF file is configured and the simulation is run in VALIDATE mode, otherwise it is ignored. -extended true - -# --------------------- gwt mst griddata --------------------- - -block griddata -name porosity -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -longname porosity -description is the mobile domain porosity, defined as the mobile domain pore volume per mobile domain volume. Additional information on porosity within the context of mobile and immobile domain transport simulations is included in the MODFLOW 6 Supplemental Technical Information document. - -block griddata -name decay -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname aqueous phase decay rate coefficient -description is the rate coefficient for first or zero-order decay for the aqueous phase of the mobile domain. A negative value indicates solute production. The dimensions of decay for first-order decay is one over time. The dimensions of decay for zero-order decay is mass per length cubed per time. decay will have no effect on simulation results unless either first- or zero-order decay is specified in the options block. - -block griddata -name decay_sorbed -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname sorbed phase decay rate coefficient -description is the rate coefficient for first or zero-order decay for the sorbed phase of the mobile domain. A negative value indicates solute production. The dimensions of decay\_sorbed for first-order decay is one over time. The dimensions of decay\_sorbed for zero-order decay is mass of solute per mass of aquifer per time. If decay\_sorbed is not specified and both decay and sorption are active, then the program will terminate with an error. decay\_sorbed will have no effect on simulation results unless the SORPTION keyword and either first- or zero-order decay are specified in the options block. - -block griddata -name bulk_density -type double precision -shape (nodes) -reader readarray -optional true -layered true -netcdf true -longname bulk density -description is the bulk density of the aquifer in mass per length cubed. bulk\_density is not required unless the SORPTION keyword is specified. Bulk density is defined as the mobile domain solid mass per mobile domain volume. Additional information on bulk density is included in the MODFLOW 6 Supplemental Technical Information document. - -block griddata -name distcoef -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname distribution coefficient -description is the distribution coefficient for the equilibrium-controlled linear sorption isotherm in dimensions of length cubed per mass. If the Freunchlich isotherm is specified, then discoef is the Freundlich constant. If the Langmuir isotherm is specified, then distcoef is the Langmuir constant. distcoef is not required unless the SORPTION keyword is specified. - -block griddata -name sp2 -type double precision -shape (nodes) -reader readarray -layered true -netcdf true -optional true -longname second sorption parameter -description is the exponent for the Freundlich isotherm and the sorption capacity for the Langmuir isotherm. sp2 is not required unless the SORPTION keyword is specified in the options block. If the SORPTION keyword is not specified in the options block, sp2 will have no effect on simulation results. - diff --git a/autotest/autotest/temp/dfn/gwt-mvt.dfn b/autotest/autotest/temp/dfn/gwt-mvt.dfn deleted file mode 100644 index 4423589f..00000000 --- a/autotest/autotest/temp/dfn/gwt-mvt.dfn +++ /dev/null @@ -1,106 +0,0 @@ -# --------------------- gwt mvt options --------------------- -# flopy subpackage mvt_filerecord mvt perioddata perioddata -# flopy parent_name_type parent_model_or_package MFModel/MFPackage - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'mover'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'lake'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save lake flows to budget file -description REPLACE save_flows {'{#1}': 'lake'} - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - - diff --git a/autotest/autotest/temp/dfn/gwt-mwt.dfn b/autotest/autotest/temp/dfn/gwt-mwt.dfn deleted file mode 100644 index c3106c4f..00000000 --- a/autotest/autotest/temp/dfn/gwt-mwt.dfn +++ /dev/null @@ -1,427 +0,0 @@ -# --------------------- gwt mwt options --------------------- -# flopy multi-package - -block options -name flow_package_name -type string -shape -reader urword -optional true -longname keyword to specify name of corresponding flow package -description keyword to specify the name of the corresponding flow package. If not specified, then the corresponding flow package must have the same name as this advanced transport package (the name associated with this package in the GWT name file). - -block options -name auxiliary -type string -shape (naux) -reader urword -optional true -longname keyword to specify aux variables -description REPLACE auxnames {'{#1}': 'Groundwater Transport'} - -block options -name flow_package_auxiliary_name -type string -shape -reader urword -optional true -longname keyword to specify name of concentration auxiliary variable in flow package -description keyword to specify the name of an auxiliary variable in the corresponding flow package. If specified, then the simulated concentrations from this advanced transport package will be copied into the auxiliary variable specified with this name. Note that the flow package must have an auxiliary variable with this name or the program will terminate with an error. If the flows for this advanced transport package are read from a file, then this option will have no effect. - -block options -name boundnames -type keyword -shape -reader urword -optional true -longname -description REPLACE boundnames {'{#1}': 'well'} - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'well'} - -block options -name print_concentration -type keyword -reader urword -optional true -longname print calculated concentrations to listing file -description REPLACE print_concentration {'{#1}': 'well', '{#2}': 'concentration', '{#3}': 'CONCENTRATION'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'well'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save well flows to budget file -description REPLACE save_flows {'{#1}': 'well'} - -block options -name concentration_filerecord -type record concentration fileout concfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name concentration -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname stage keyword -description keyword to specify that record corresponds to concentration. - -block options -name concfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write concentration information. - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the binary output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name ts_filerecord -type record ts6 filein ts6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name ts6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname head keyword -description keyword to specify that record corresponds to a time-series file. - -block options -name filein -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name ts6_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname file name of time series information -description REPLACE timeseriesfile {} - -block options -name obs_filerecord -type record obs6 filein obs6_filename -shape -reader urword -tagged true -optional true -longname -description - -block options -name obs6 -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname obs keyword -description keyword to specify that record corresponds to an observations file. - -block options -name obs6_filename -type string -preserve_case true -in_record true -tagged false -reader urword -optional false -longname obs6 input filename -description REPLACE obs6_filename {'{#1}': 'MWT', '{#2}': '\\ref{table:gwt-obstypetable}'} - - -# --------------------- gwt mwt packagedata --------------------- - -block packagedata -name packagedata -type recarray ifno strt aux boundname -shape (maxbound) -reader urword -longname -description - -block packagedata -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname well number for this entry -description integer value that defines the feature (well) number associated with the specified PACKAGEDATA data on the line. IFNO must be greater than zero and less than or equal to NMAWWELLS. Well information must be specified for every well or the program will terminate with an error. The program will also terminate with an error if information for a well is specified more than once. -numeric_index true - -block packagedata -name strt -type double precision -shape -tagged false -in_record true -reader urword -longname starting well concentration -description real value that defines the starting concentration for the well. - -block packagedata -name aux -type double precision -in_record true -tagged false -shape (naux) -reader urword -time_series true -optional true -longname auxiliary variables -description REPLACE aux {'{#1}': 'well'} - -block packagedata -name boundname -type string -shape -tagged false -in_record true -reader urword -optional true -longname well name -description REPLACE boundname {'{#1}': 'well'} - - -# --------------------- gwt mwt period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name mwtperioddata -type recarray ifno mwtsetting -shape -reader urword -longname -description - -block period -name ifno -type integer -shape -tagged false -in_record true -reader urword -longname well number for this entry -description integer value that defines the feature (well) number associated with the specified PERIOD data on the line. IFNO must be greater than zero and less than or equal to NMAWWELLS. -numeric_index true - -block period -name mwtsetting -type keystring status concentration rate auxiliaryrecord -shape -tagged false -in_record true -reader urword -longname -description line of information that is parsed into a keyword and values. Keyword values that can be used to start the MWTSETTING string include: STATUS, CONCENTRATION, RATE, and AUXILIARY. These settings are used to assign the concentration associated with the corresponding flow terms. Concentrations cannot be specified for all flow terms. For example, the Multi-Aquifer Well Package supports a ``WITHDRAWAL'' flow term. If this withdrawal term is active, then water will be withdrawn from the well at the calculated concentration of the well. - -block period -name status -type string -shape -tagged true -in_record true -reader urword -longname well concentration status -description keyword option to define well status. STATUS can be ACTIVE, INACTIVE, or CONSTANT. By default, STATUS is ACTIVE, which means that concentration will be calculated for the well. If a well is inactive, then there will be no solute mass fluxes into or out of the well and the inactive value will be written for the well concentration. If a well is constant, then the concentration for the well will be fixed at the user specified value. - -block period -name concentration -type string -shape -tagged true -in_record true -time_series true -reader urword -longname well concentration -description real or character value that defines the concentration for the well. The specified CONCENTRATION is only applied if the well is a constant concentration well. 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. - -block period -name rate -type string -shape -tagged true -in_record true -reader urword -time_series true -longname well injection concentration -description real or character value that defines the injection solute concentration $(ML^{-3})$ for the well. 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. - -block period -name auxiliaryrecord -type record auxiliary auxname auxval -shape -tagged -in_record true -reader urword -longname -description - -block period -name auxiliary -type keyword -shape -in_record true -reader urword -longname -description keyword for specifying auxiliary variable. - -block period -name auxname -type string -shape -tagged false -in_record true -reader urword -longname -description name for the auxiliary variable to be assigned AUXVAL. AUXNAME must match one of the auxiliary variable names defined in the OPTIONS block. If AUXNAME does not match one of the auxiliary variable names defined in the OPTIONS block the data are ignored. - -block period -name auxval -type double precision -shape -tagged false -in_record true -reader urword -time_series true -longname auxiliary variable value -description value for the auxiliary variable. 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. diff --git a/autotest/autotest/temp/dfn/gwt-nam.dfn b/autotest/autotest/temp/dfn/gwt-nam.dfn deleted file mode 100644 index 9645f870..00000000 --- a/autotest/autotest/temp/dfn/gwt-nam.dfn +++ /dev/null @@ -1,210 +0,0 @@ -# --------------------- gwt nam options --------------------- - -block options -name list -type string -reader urword -optional true -preserve_case true -longname name of listing file -description is name of the listing file to create for this GWT model. If not specified, then the name of the list file will be the basename of the GWT model name file and the '.lst' extension. For example, if the GWT name file is called ``my.model.nam'' then the list file will be called ``my.model.lst''. - -block options -name print_input -type keyword -reader urword -optional true -longname print input to listing file -description REPLACE print_input {'{#1}': 'all model stress package'} - -block options -name print_flows -type keyword -reader urword -optional true -longname print calculated flows to listing file -description REPLACE print_flows {'{#1}': 'all model package'} - -block options -name save_flows -type keyword -reader urword -optional true -longname save flows for all packages to budget file -description REPLACE save_flows {'{#1}': 'all model package'} - -block options -name dependent_variable_scaling -type keyword -reader urword -optional true -longname flag to scale X and RHS -description flag to scale X and RHS to avoid very large positive or negative dependent variable values -mf6internal idv_scale - -block options -name nc_mesh2d_filerecord -type record netcdf_mesh2d fileout ncmesh2dfile -shape -reader urword -tagged true -optional true -longname -description NetCDF layered mesh fileout record. -mf6internal ncmesh2drec - -block options -name netcdf_mesh2d -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a layered mesh NetCDF file. -extended true - -block options -name nc_structured_filerecord -type record netcdf_structured fileout ncstructfile -shape -reader urword -tagged true -optional true -longname -description NetCDF structured fileout record. -mf6internal ncstructrec - -block options -name netcdf_structured -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to a structured NetCDF file. -mf6internal netcdf_struct -extended true - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name ncmesh2dfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF ugrid layered mesh output file. -extended true - -block options -name ncstructfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the NetCDF structured output file. -extended true - -block options -name nc_filerecord -type record netcdf filein netcdf_filename -reader urword -tagged true -optional true -longname -description NetCDF filerecord - -block options -name netcdf -type keyword -in_record true -reader urword -tagged true -optional false -longname netcdf keyword -description keyword to specify that record corresponds to a NetCDF input file. -extended true - -block options -name filein -type keyword -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an input filename is expected next. - -block options -name netcdf_filename -type string -preserve_case true -in_record true -reader urword -optional false -tagged false -longname netcdf input filename -description defines a NetCDF input file. -mf6internal netcdf_fname -extended true - -# --------------------- gwt nam packages --------------------- - -block packages -name packages -type recarray ftype fname pname -reader urword -optional false -longname package list -description - -block packages -name ftype -in_record true -type string -tagged false -reader urword -longname package type -description is the file type, which must be one of the following character values shown in table~\ref{table:ftype-gwt}. Ftype may be entered in any combination of uppercase and lowercase. - -block packages -name fname -in_record true -type string -preserve_case true -tagged false -reader urword -longname file name -description is the name of the file containing the package input. The path to the file should be included if the file is not located in the folder where the program was run. - -block packages -name pname -in_record true -type string -tagged false -reader urword -optional true -longname user name for package -description is the user-defined name for the package. PNAME is restricted to 16 characters. No spaces are allowed in PNAME. PNAME character values are read and stored by the program for stress packages only. These names may be useful for labeling purposes when multiple stress packages of the same type are located within a single GWT Model. If PNAME is specified for a stress package, then PNAME will be used in the flow budget table in the listing file; it will also be used for the text entry in the cell-by-cell budget file. PNAME is case insensitive and is stored in all upper case letters. - diff --git a/autotest/autotest/temp/dfn/gwt-oc.dfn b/autotest/autotest/temp/dfn/gwt-oc.dfn deleted file mode 100644 index bae202eb..00000000 --- a/autotest/autotest/temp/dfn/gwt-oc.dfn +++ /dev/null @@ -1,318 +0,0 @@ -# --------------------- gwt oc options --------------------- - -block options -name budget_filerecord -type record budget fileout budgetfile -shape -reader urword -tagged true -optional true -mf6internal budfilerec -longname -description - -block options -name budget -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget. - -block options -name fileout -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname file keyword -description keyword to specify that an output filename is expected next. - -block options -name budgetfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the output file to write budget information. - -block options -name budgetcsv_filerecord -type record budgetcsv fileout budgetcsvfile -shape -reader urword -tagged true -optional true -mf6internal budcsvfilerec -longname -description - -block options -name budgetcsv -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname budget keyword -description keyword to specify that record corresponds to the budget CSV. - -block options -name budgetcsvfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -description name of the comma-separated value (CSV) output file to write budget summary information. A budget summary record will be written to this file for each time step of the simulation. - -block options -name concentration_filerecord -type record concentration fileout concentrationfile -shape -reader urword -tagged true -optional true -mf6internal concfilerec -longname -description - -block options -name concentration -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname concentration keyword -description keyword to specify that record corresponds to concentration. - -block options -name concentrationfile -type string -preserve_case true -shape -in_record true -reader urword -tagged false -optional false -longname file keyword -mf6internal concfile -description name of the output file to write conc information. - -block options -name concentrationprintrecord -type record concentration print_format formatrecord -shape -reader urword -optional true -mf6internal concprintrec -longname -description - -block options -name print_format -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to indicate that a print format follows -description keyword to specify format for printing to the listing file. - -block options -name formatrecord -type record columns width digits format -shape -in_record true -reader urword -tagged -optional false -longname -description - -block options -name columns -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of columns -description number of columns for writing data. - -block options -name width -type integer -shape -in_record true -reader urword -tagged true -optional -longname width for each number -description width for writing each number. - -block options -name digits -type integer -shape -in_record true -reader urword -tagged true -optional -longname number of digits -description number of digits to use for writing a number. - -block options -name format -type string -shape -in_record true -reader urword -tagged false -optional false -longname write format -description write format can be EXPONENTIAL, FIXED, GENERAL, or SCIENTIFIC. - - -# --------------------- gwt oc period --------------------- - -block period -name iper -type integer -block_variable true -in_record true -tagged false -shape -valid -reader urword -optional false -longname stress period number -description REPLACE iper {} - -block period -name saverecord -type record save rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name save -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be saved this stress period. - -block period -name printrecord -type record print rtype ocsetting -shape -reader urword -tagged false -optional true -longname -description - -block period -name print -type keyword -shape -in_record true -reader urword -tagged true -optional false -longname keyword to save -description keyword to indicate that information will be printed this stress period. - -block period -name rtype -type string -shape -in_record true -reader urword -tagged false -optional false -longname record type -description type of information to save or print. Can be BUDGET or CONCENTRATION. - -block period -name ocsetting -type keystring all first last frequency steps -shape -tagged false -in_record true -reader urword -longname -description specifies the steps for which the data will be saved. - -block period -name all -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for all time steps in period. - -block period -name first -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for first step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name last -type keyword -shape -in_record true -reader urword -longname -description keyword to indicate save for last step in period. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name frequency -type integer -shape -tagged true -in_record true -reader urword -longname -description save at the specified time step frequency. This keyword may be used in conjunction with other keywords to print or save results for multiple time steps. - -block period -name steps -type integer -shape ( Date: Fri, 22 May 2026 05:38:41 -0700 Subject: [PATCH 18/29] appease mypy, drop official release from dfns.toml (only nightly build has the dfns asset for now) --- modflow_devtools/dfn/schema.py | 5 ++++- modflow_devtools/dfns/dfns.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modflow_devtools/dfn/schema.py b/modflow_devtools/dfn/schema.py index 64a4d942..d428695d 100644 --- a/modflow_devtools/dfn/schema.py +++ b/modflow_devtools/dfn/schema.py @@ -754,7 +754,10 @@ def get_fields(dfn: Dfn) -> OMD: def _has_grid_dependent_shapes(dfn: Dfn) -> bool: """Return True if any field uses a semicolon grid-type-dependent shape.""" - for block in dfn.get("blocks", {}).values(): + blocks = dfn.get("blocks", {}) + if not blocks: + return False + for block in blocks.values(): for field in block.values(): if ";" in str(field.get("shape") or ""): return True diff --git a/modflow_devtools/dfns/dfns.toml b/modflow_devtools/dfns/dfns.toml index 7458bfd0..5676b5cc 100644 --- a/modflow_devtools/dfns/dfns.toml +++ b/modflow_devtools/dfns/dfns.toml @@ -4,6 +4,6 @@ # - Windows: %APPDATA%/modflow-devtools/dfns.toml releases = [ - "MODFLOW-ORG/modflow6@latest", + # "MODFLOW-ORG/modflow6@latest", "MODFLOW-ORG/modflow6-nightly-build@latest" ] From ff348ccfa81db2818626e066724d691aad2ae0e3 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 05:44:56 -0700 Subject: [PATCH 19/29] drop python 3.10 --- .github/workflows/ci.yml | 4 ++-- DEVELOPER.md | 2 +- README.md | 2 +- pyproject.toml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb44a15f..ac648fdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-22.04, macos-14, windows-2022 ] - python: [ "3.10", "3.11", "3.12", "3.13" ] + python: [ "3.11", "3.12", "3.13", "3.14" ] env: GCC_V: 11 steps: @@ -133,7 +133,7 @@ jobs: # only invoke the GH API on one OS and Python version # to avoid rate limits (1000 rqs / hour / repository) # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits - if: runner.os == 'Linux' && matrix.python == '3.10' + if: runner.os == 'Linux' && matrix.python == '3.11' working-directory: modflow-devtools/autotest env: REPOS_PATH: ${{ github.workspace }} diff --git a/DEVELOPER.md b/DEVELOPER.md index b05fb98a..8d91d627 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -19,7 +19,7 @@ This document provides guidance to set up a development environment and discusse ## Requirements -Python3.10+ is currently required. This project has historically aimed to support several recent versions of Python, loosely following [NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html#implementation). In current and future development this window may narrow to follow [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) instead. +Python3.11+. This project has historically aimed to support several recent versions of Python, loosely following [NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html#implementation). In current and future development this window may narrow to follow [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) instead. ## Installation diff --git a/README.md b/README.md index 9c2dad64..15159ef5 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Python development tools for MODFLOW 6 and related projects. ## Requirements -Python3.10+, dependency-free by default. +Python3.11+, dependency-free by default. Two main dependency groups are available, oriented around specific use cases: diff --git a/pyproject.toml b/pyproject.toml index 8e621884..cf7f492a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,13 +29,13 @@ classifiers = [ "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Hydrology" ] -requires-python = ">=3.10" +requires-python = ">=3.11" dynamic = ["version"] [project.optional-dependencies] From ab67123632f6397ba47b294ecad888e03c0f2fdb Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 05:46:28 -0700 Subject: [PATCH 20/29] ruff --- autotest/test_programs.py | 5 +++-- modflow_devtools/programs/__init__.py | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autotest/test_programs.py b/autotest/test_programs.py index bc3e7dfc..9a6a2cd9 100644 --- a/autotest/test_programs.py +++ b/autotest/test_programs.py @@ -1,4 +1,5 @@ import warnings +from datetime import UTC from pathlib import Path import pytest @@ -304,7 +305,7 @@ def test_program_manager_error_handling(self): def test_installation_metadata_integration(self): """Test InstallationMetadata integration with ProgramManager.""" - from datetime import datetime, timezone + from datetime import datetime from pathlib import Path from modflow_devtools.programs import ( @@ -322,7 +323,7 @@ def test_installation_metadata_integration(self): version="1.0.0", platform="linux", bindir=Path("/tmp/test"), - installed_at=datetime.now(timezone.utc), + installed_at=datetime.now(UTC), source={ "repo": "test/repo", "tag": "1.0.0", diff --git a/modflow_devtools/programs/__init__.py b/modflow_devtools/programs/__init__.py index 1de99248..19f04399 100644 --- a/modflow_devtools/programs/__init__.py +++ b/modflow_devtools/programs/__init__.py @@ -3,7 +3,7 @@ import shutil import warnings from dataclasses import dataclass, field -from datetime import datetime +from datetime import UTC, datetime from os import PathLike from pathlib import Path @@ -1416,7 +1416,6 @@ def install( If installation fails """ import shutil - from datetime import timezone # 1. Load config and find program in registries config = self.config @@ -1576,7 +1575,7 @@ def install( version=version, platform=platform, bindir=bindir, - installed_at=datetime.now(timezone.utc), + installed_at=datetime.now(UTC), source=source_info, executables=[exe_name], ) From 715a765cbaf081828996ac6ec75602cfbedc6313 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 06:04:43 -0700 Subject: [PATCH 21/29] fix test paths, fix uv venv path, add intro language to dfn spec --- .github/workflows/ci.yml | 7 +++---- docs/md/dfnspec.md | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac648fdc..926dead4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,12 +83,11 @@ jobs: python: [ "3.11", "3.12", "3.13", "3.14" ] env: GCC_V: 11 + UV_PROJECT_ENVIRONMENT: ${{ github.workspace }}/.venv steps: - name: Checkout repo uses: actions/checkout@v4 - with: - path: modflow-devtools - name: Checkout modflow6 for DFN autodiscovery uses: actions/checkout@v4 @@ -127,7 +126,7 @@ jobs: DFNS_PATH: ${{ github.workspace }}/modflow6/doc/mf6io/mf6ivar/dfn MODFLOW_DEVTOOLS_AUTO_SYNC: 0 # use --dist loadfile to so tests requiring pytest-virtualenv run on the same worker - run: uv run pytest -v -n auto --dist loadfile --durations 0 --ignore test_download.py --ignore test_models.py --ignore test_dfns_registry.py + run: uv run pytest -v -n auto --dist loadfile --durations 0 --ignore test_download.py --ignore test_models.py --ignore dfns/test_dfns_registry.py - name: Run network-dependent tests # only invoke the GH API on one OS and Python version @@ -151,7 +150,7 @@ jobs: TEST_PROGRAMS_REPO: MODFLOW-ORG/modflow6 TEST_PROGRAMS_REF: develop TEST_PROGRAMS_SOURCE: modflow6 - run: uv run pytest -v -n auto --dist loadgroup --durations 0 test_download.py test_models.py test_dfns_registry.py + run: uv run pytest -v -n auto --dist loadgroup --durations 0 test_download.py test_models.py dfns/test_dfns_registry.py rtd: name: Docs diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index e03a3f78..3bfc85c0 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -1,5 +1,7 @@ # DFN specification +This document describes the MODFLOW 6 component definition (DFN) system. This system is used to specify MODFLOW 6 components and their inputs, and reflects the MODFLOW 6 input data model as described in the MF6 IO guide. + - [Overview](#overview) - [Components](#components) - [Shared attributes](#shared-attributes) From f10c7b0d360dd39caa77cf10bc80983ebd72dfff Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 06:07:17 -0700 Subject: [PATCH 22/29] tweak language --- docs/md/dfnspec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 3bfc85c0..64800b1b 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -87,7 +87,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy A MODFLOW 6 simulation consists of a hierarchy of components, each one representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. -Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component and its fields, relationships between fields or to other components, and data representations and in some cases formatting information. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. +Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component, including its fields, relationships between fields or to other components, and data representations. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. ## Components From caea4222c983b3f908f61d30a1f1ff6f5a79c605 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 06:48:19 -0700 Subject: [PATCH 23/29] wip --- autotest/dfns/test_dfns_schema.py | 140 +++++++++++++++--------------- docs/md/dfnspec.md | 88 ++++++++----------- modflow_devtools/dfns/mapper.py | 28 +++--- modflow_devtools/dfns/schema.py | 8 +- 4 files changed, 125 insertions(+), 139 deletions(-) diff --git a/autotest/dfns/test_dfns_schema.py b/autotest/dfns/test_dfns_schema.py index 9899e864..be7cea81 100644 --- a/autotest/dfns/test_dfns_schema.py +++ b/autotest/dfns/test_dfns_schema.py @@ -8,7 +8,7 @@ from modflow_devtools.dfns.schema import ( Array, Block, - DimDef, + Dim, Double, FieldBase, Integer, @@ -323,9 +323,9 @@ def test_local_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -340,7 +340,7 @@ def test_local_dims(): pkg3 = Package( name="test", blocks=None, - dims={"nodes": DimDef(expr="42", scope="component")}, + dims={"nodes": Dim(expr="42", scope="component")}, ) spec3 = Dfns(components={"test": pkg3}) assert spec3.local_dims("test") == {"nodes"} @@ -429,10 +429,10 @@ def test_resolve_derived_dims(): name="test", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="component"), - "nrow": DimDef(field="nrow", scope="component"), - "ncol": DimDef(field="ncol", scope="component"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="component"), + "nlay": Dim(field="nlay", scope="component"), + "nrow": Dim(field="nrow", scope="component"), + "ncol": Dim(field="ncol", scope="component"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="component"), }, ) order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) @@ -442,11 +442,11 @@ def test_resolve_derived_dims(): name="test", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="component"), - "nrow": DimDef(field="nrow", scope="component"), - "ncol": DimDef(field="ncol", scope="component"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="component"), - "nodouble": DimDef(expr="nodes * 2", scope="component"), + "nlay": Dim(field="nlay", scope="component"), + "nrow": Dim(field="nrow", scope="component"), + "ncol": Dim(field="ncol", scope="component"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="component"), + "nodouble": Dim(expr="nodes * 2", scope="component"), }, ) order = _resolve_derived_dims(pkg, {"nlay", "nrow", "ncol"}) @@ -455,7 +455,7 @@ def test_resolve_derived_dims(): pkg = Package( name="test", blocks=None, - dims={"derived": DimDef(expr="nodes + 1", scope="component")}, + dims={"derived": Dim(expr="nodes + 1", scope="component")}, ) order = _resolve_derived_dims(pkg, {"nodes"}) assert order == ["derived"] @@ -466,7 +466,7 @@ def test_resolve_derived_dims_sum_operand_allowed(): pkg = Package( name="test", blocks=pkg.blocks, - dims={"total_conn": DimDef(expr="sum(packagedata.nlakeconn)", scope="component")}, + dims={"total_conn": Dim(expr="sum(packagedata.nlakeconn)", scope="component")}, ) order = _resolve_derived_dims(pkg, set()) assert order == ["total_conn"] @@ -482,8 +482,8 @@ def test_resolve_derived_dims_cycle_error(): name="test", blocks=None, dims={ - "a": DimDef(expr="b + 1", scope="component"), - "b": DimDef(expr="a + 1", scope="component"), + "a": Dim(expr="b + 1", scope="component"), + "b": Dim(expr="a + 1", scope="component"), }, ) with pytest.raises(ValueError, match="Cycle in"): @@ -494,7 +494,7 @@ def test_resolve_derived_dims_unknown_operand_error(): pkg = Package( name="test", blocks=None, - dims={"nodes": DimDef(expr="mystery_dim * 2", scope="component")}, + dims={"nodes": Dim(expr="mystery_dim * 2", scope="component")}, ) with pytest.raises(ValueError, match="not a known dimension"): _resolve_derived_dims(pkg, set()) @@ -504,7 +504,7 @@ def test_resolve_derived_dims_invalid_expression_error(): pkg = Package( name="test", blocks=None, - dims={"nodes": DimDef(expr="nlay * (", scope="component")}, + dims={"nodes": Dim(expr="nlay * (", scope="component")}, ) with pytest.raises(ValueError, match="Invalid"): _resolve_derived_dims(pkg, set()) @@ -516,10 +516,10 @@ def test_dfnspec_construction_validates_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -531,8 +531,8 @@ def test_dfnspec_construction_cycle_raises(): name="bad", blocks=None, dims={ - "a": DimDef(expr="b + 1", scope="component"), - "b": DimDef(expr="a + 1", scope="component"), + "a": Dim(expr="b + 1", scope="component"), + "b": Dim(expr="a + 1", scope="component"), }, ) with pytest.raises(ValueError, match="Cycle in"): @@ -543,7 +543,7 @@ def test_dfnspec_construction_unknown_operand_raises(): pkg = Package( name="bad", blocks=None, - dims={"nodes": DimDef(expr="ghost_dim * 2", scope="component")}, + dims={"nodes": Dim(expr="ghost_dim * 2", scope="component")}, ) with pytest.raises(ValueError, match="not a known dimension"): Dfns(components={"bad": pkg}) @@ -566,9 +566,9 @@ def test_dfnspec_local_dims(): name="gwf-dis", blocks={"dimensions": block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) spec = Dfns(components={"gwf-dis": pkg}) @@ -588,10 +588,10 @@ def test_dfnspec_inherited_dims_includes_dis_dims(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -612,8 +612,8 @@ def test_dfnspec_inherited_dims_disv(): parent="gwf-nam", blocks={"dimensions": disv_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "ncpl": DimDef(field="ncpl", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "ncpl": Dim(field="ncpl", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -632,8 +632,8 @@ def test_dfnspec_inherited_dims_disu(): parent="gwf-nam", blocks={"dimensions": disu_block}, dims={ - "nodes": DimDef(field="nodes", scope="model"), - "nja": DimDef(field="nja", scope="model"), + "nodes": Dim(field="nodes", scope="model"), + "nja": Dim(field="nja", scope="model"), }, ) chd = _pkg("gwf-chd", parent="gwf-nam", blocks=None) @@ -653,16 +653,16 @@ def test_dfnspec_inherited_dims_excludes_own(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) chd = Package( name="gwf-chd", parent="gwf-nam", blocks={"dimensions": _dim_block("secret_dim")}, - dims={"secret_dim": DimDef(field="secret_dim", scope="model")}, + dims={"secret_dim": Dim(field="secret_dim", scope="model")}, ) gwf = Model(name="gwf-nam", blocks=None) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis, "gwf-chd": chd}) @@ -754,10 +754,10 @@ def _dis_spec() -> Dfns: parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) return Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -798,10 +798,10 @@ def test_dims_includes_derived(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) spec = Dfns(components={"gwf-nam": gwf, "gwf-dis": dis}) @@ -826,9 +826,9 @@ def test_dims_includes_model_scoped(): def _make_ctx(dim_names: set[str], derived: dict | None = None): """Return (array, component, known_dims) for shape element tests.""" - dims: dict[str, DimDef] = {n: DimDef(field=n, scope="component") for n in dim_names} + dims: dict[str, Dim] = {n: Dim(field=n, scope="component") for n in dim_names} if derived: - dims.update({n: DimDef(expr=e, scope="component") for n, e in derived.items()}) + dims.update({n: Dim(expr=e, scope="component") for n, e in derived.items()}) blocks = {"dimensions": _dim_block(*dim_names)} if dim_names else None pkg = Package(name="test", blocks=blocks, dims=dims or None) gwf = Model(name="gwf-nam", blocks=None) @@ -849,7 +849,7 @@ def test_shape_element_valid_inherited_dim(): name="gwf-dis", parent="gwf-nam", blocks=None, - dims={"nodes": DimDef(expr="42", scope="model")}, + dims={"nodes": Dim(expr="42", scope="model")}, ) test_pkg = Package(name="gwf-test", parent="gwf-nam", blocks=None) gwf = Model(name="gwf-nam", blocks=None) @@ -1010,9 +1010,9 @@ def test_dfnspec_valid_top_level_array_shape(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1030,9 +1030,9 @@ def test_dfnspec_valid_array_in_record(): parent="gwf-nam", blocks={"dimensions": dis_block, "options": opt_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1070,9 +1070,9 @@ def test_dfnspec_invalid_array_shape_raises(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1089,10 +1089,10 @@ def test_dfnspec_array_shape_resolves_via_derived_dim(): parent="gwf-nam", blocks={"dimensions": dis_block, "griddata": grid_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) gwf = Model(name="gwf-nam", blocks=None) @@ -1107,10 +1107,10 @@ def test_dfnspec_array_shape_resolves_via_sibling_dis(): parent="gwf-nam", blocks={"dimensions": dis_block}, dims={ - "nlay": DimDef(field="nlay", scope="model"), - "nrow": DimDef(field="nrow", scope="model"), - "ncol": DimDef(field="ncol", scope="model"), - "nodes": DimDef(expr="nlay * nrow * ncol", scope="model"), + "nlay": Dim(field="nlay", scope="model"), + "nrow": Dim(field="nrow", scope="model"), + "ncol": Dim(field="ncol", scope="model"), + "nodes": Dim(expr="nlay * nrow * ncol", scope="model"), }, ) chd_arr = Array(name="head", dtype="double", shape=["nlay", "nodes"]) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 64800b1b..642dde02 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -85,13 +85,15 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy ## Overview -A MODFLOW 6 simulation consists of a hierarchy of components, each one representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. +A MODFLOW 6 simulation consists of a hierarchy of modules, each one representing some functional element, such as a grid discretization, a hydrologic process (i.e. model), or a boundary condition. -Each component is defined by a **component definition** (DFN), which specifies the valid contents of the component's input file. A definition characterizes the component, including its fields, relationships between fields or to other components, and data representations. A definition is one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. +Modules are specified by **component definitions** (DFNs), each of which describes the module's general properties, its input fields, and its relationships to other modules. A module be represented by more than one component definition. A definition describes one way of representing a module; it may not be the only way. Any number of representational variants may exist, each of which reflects a certain tradeoff between properties like program runtime, memory or disk usage, and convenience. + +This document refers to **components** instead of modules to emphasize this distinction. ## Components -Component definitions consist primarily of a name, zero or more block definitions, as well as other optional attributes. +Component definitions consist of a number of attributes: - `type`: the component type (`"simulation"`, `"model"`, or `"package"`) - `name`: the component's name @@ -448,12 +450,12 @@ Type `list`. Collection type. Unlimited but for one rule: a list may not contain ### Array dimensions -Dimensions are declared at the component level via the `dims` map, not on individual fields. Each entry in `dims` is a `DimDef`: +Dimensions are declared at the component level via the `dims` map, not on individual fields. Each entry in `dims` consists of a source (a field or an expression combining other dimension fields) and a scope: ```yaml dims: nlay: - field: nlay # backed by an integer field named 'nlay' in this component + field: nlay # backed by a sibling integer field 'nlay' scope: model nodes: expr: "nlay * nrow * ncol" # derived from other dims @@ -463,63 +465,49 @@ dims: scope: simulation ``` -A `DimDef` has exactly one of: -- `field`: the name of an `integer` field in this component that provides the dimension value -- `expr`: a Python arithmetic expression that derives the dimension from other known dims - -And a `scope` (see [Scope and resolution](#scope-and-resolution)) that controls which other components can see it. - -Self-sizing `array` fields (those with `shape: []`) may also serve as dimension sources: any such array's name may appear in a `shape` expression to mean "one element per item in this array." These are registered in `dims` with `field` pointing to the array name. - -Shape expressions for non-string arrays may use one of four structural forms. Dim references may additionally carry a bound annotation: - -- **Dim reference** (`^[A-Za-z_]\w*$`): a plain identifier resolved via the scope chain (explicit → derived → inherited dims). When the array is a subfield of a record and the identifier does not resolve globally, resolution falls back to intra-record sibling scope (see below). -- **Intra-record sibling reference**: a dim reference that names a sibling `integer` in the same enclosing record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. See below. -- **Arithmetic offset** (`dim [+-] integer`): a dim reference with an integer offset, e.g. `nlay + 1`. Only the dim portion is validated; the offset is accepted as-is. -- **Row-level column lookup** (`block.column(fk_field)`): a cross-list per-row quantity, valid only for array subfields of records. See below. - -Any dim reference (including the dim portion of an arithmetic offset) may carry a **bound annotation** prefix (`<`, `>`, `<=`, or `>=`). The dim portion validates normally; the bound is advisory and is not enforced by the MF6 parser. +The `field` and `expr` attributes are mutually exclusive, distinguishing explicit field-backed dimensions from derived dimensions: +- `field`: the name of an `integer` or array field in this component that provides the dimension value +- `expr`: an arithmetic expression that derives the dimension from other known dims -A shape expression that does not match one of these forms is a schema validation error. String arrays (`dtype: "string"`) must have empty `shape`. +The `scope` (see [Scope and resolution](#scope-and-resolution)) controls which other components can see the dimension. -#### Derived dimensions +Integer fields or self-sizing `array` fields (those with empty or absent `shape`) may serve as explicit dimensions. An integer field directly indicates the dimension size, whereas a self-sizing array indicate "one element per item in this array." -A `DimDef` with an `expr` rather than a `field` is a derived dimension. Its expression uses Python arithmetic syntax. Operands may be: +A dimension with an `expr` attribute rather than a `field` is a derived dimension. Shape expressions use Python-like syntax and may contain several kinds of reference: -- Explicit dimensions: any field-backed dim in this component's `dims` -- Other derived dims: another derived dim in this component; circular dependencies are a schema error -- Functions of columns in a tabular list block: `sum(block.list.column)` sums the integer values of `column` across all rows of list field `list` in block `block`. When the list field shares its name with its containing block — the MF6 convention — the block qualifier may be omitted: `sum(list.column)`. +- **Dimension reference** (`^[A-Za-z_]\w*$`): names a dimension either resolved locally or inherited from another component. +- **Record subfield reference**: names a sibling `integer` subfield in the same record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. +- **List column reference** (`block.list.column(fk_field)`): names a subfield of a record which is the item type of a regular (i.e. tabular) list. -Derived dims carry the same `scope` as field-backed dims and are visible to other components under the same rules. +Shape expressions may also include simple integer arithmetic, e.g. `nlay + 1`, as well as constraints, e.g. `<`, `>`, `<=`, or `>=` and simple math functions like `sum()`. -Canonical examples: +Canonical examples of explicit and derived dimensions: ```yaml dims: - # gwf-dis: nodes and ncpl are derived to give packages a uniform dim - # regardless of discretization type (DISV has ncpl explicit; DIS derives it) - nlay: {field: nlay, scope: model} - nrow: {field: nrow, scope: model} - ncol: {field: ncol, scope: model} - ncpl: {expr: "nrow * ncol", scope: model} - nodes: {expr: "nlay * nrow * ncol", scope: model} - ncelldim: {expr: "3", scope: model} - - # gwf-disv: ncpl is explicit; nodes is derived - ncpl: {field: ncpl, scope: model} - nodes: {expr: "nlay * ncpl", scope: model} - ncelldim: {expr: "2", scope: model} + # sim-tdis + nper: {field: nper, scope: simulation} + + # gwf-dis + nlay: {field: nlay, scope: model} + nrow: {field: nrow, scope: model} + ncol: {field: ncol, scope: model} + ncpl: {expr: "nrow * ncol", scope: model} + nodes: {expr: "nlay * nrow * ncol", scope: model} + ncelldim: {expr: "3", scope: model} + + # gwf-disv + ncpl: {field: ncpl, scope: model} + nodes: {expr: "nlay * ncpl", scope: model} + ncelldim: {expr: "2", scope: model} # gwf-lak total_lake_connections: {expr: "sum(packagedata.nlakeconn)", scope: component} - - # sim-tdis - nper: {field: nper, scope: simulation} ``` -#### Row-level column lookups +#### Inline arrays -An array appearing as a subfield of a record may have its size determined per row by a value in another list. The shape expression form for this is: +An array appearing as a subfield of a record is called an **inline array**. An inline array may have its size determined by an integer subfield in the same record, or by a column in another list, . The shape expression form for this is: ``` block.column(fk_field) @@ -539,9 +527,7 @@ Validation rules: - `column` must exist in `block`'s item record and be of type `integer` - This form is only valid when the array is a subfield of a record; it is a schema error on a top-level array field -Unlike derived dimensions, row-level lookups are not pre-computable at load time. They are evaluated per row during parsing. - -Canonical example — `gwf-sfr.connectiondata.ic`, whose length varies per reach according to the `ncon` column in `packagedata`: +The canonical example of this is `gwf-sfr.connectiondata.ic`, an array field whose length varies per reach according to the `ncon` column in `packagedata`: ```yaml packagedata: @@ -623,7 +609,7 @@ Row-level column lookups and bound annotations (` dict[str, v2.DimDef]: +) -> dict[str, v2.Dim]: """Build the dims section from a component's dimensions block.""" - dims: dict[str, v2.DimDef] = {} + dims: dict[str, v2.Dim] = {} dim_block = blocks.get("dimensions") if not dim_block: return dims @@ -44,30 +44,30 @@ def _build_explicit_dims( scope = _scope_for(parent) for fname, field in dim_block.fields.items(): if isinstance(field, v2.Integer): - dims[fname] = v2.DimDef(field=fname, scope=scope) + dims[fname] = v2.Dim(field=fname, scope=scope) if scope == "model": has = set(dims.keys()) if {"nlay", "nrow", "ncol"} <= has: - dims["ncpl"] = v2.DimDef(expr="nrow * ncol", scope="model") - dims["nodes"] = v2.DimDef(expr="nlay * nrow * ncol", scope="model") - dims["ncelldim"] = v2.DimDef(expr="3", scope="model") + dims["ncpl"] = v2.Dim(expr="nrow * ncol", scope="model") + dims["nodes"] = v2.Dim(expr="nlay * nrow * ncol", scope="model") + dims["ncelldim"] = v2.Dim(expr="3", scope="model") elif {"nlay", "ncpl"} <= has: - dims["nodes"] = v2.DimDef(expr="nlay * ncpl", scope="model") - dims["ncelldim"] = v2.DimDef(expr="2", scope="model") + dims["nodes"] = v2.Dim(expr="nlay * ncpl", scope="model") + dims["ncelldim"] = v2.Dim(expr="2", scope="model") elif {"nrow", "ncol"} <= has: - dims["ncpl"] = v2.DimDef(expr="nrow * ncol", scope="model") - dims["nodes"] = v2.DimDef(expr="nrow * ncol", scope="model") - dims["ncelldim"] = v2.DimDef(expr="2", scope="model") + dims["ncpl"] = v2.Dim(expr="nrow * ncol", scope="model") + dims["nodes"] = v2.Dim(expr="nrow * ncol", scope="model") + dims["ncelldim"] = v2.Dim(expr="2", scope="model") elif "nodes" in has: - dims["ncelldim"] = v2.DimDef(expr="1", scope="model") + dims["ncelldim"] = v2.Dim(expr="1", scope="model") return dims def _resolve_dimensions( blocks: dict[str, v2.Block], -) -> tuple[dict[str, v2.Block], dict[str, v2.DimDef]]: +) -> tuple[dict[str, v2.Block], dict[str, v2.Dim]]: """ Detect self-sizing arrays whose name is referenced in another array's shape expression — those define a component-scoped dimension. @@ -99,7 +99,7 @@ def _scan(fields: Mapping[str, v2.Field]) -> None: _scan(block.fields) array_dim_names = self_sizing & shape_refs - array_dims = {n: v2.DimDef(field=n, scope="component") for n in array_dim_names} + array_dims = {n: v2.Dim(field=n, scope="component") for n in array_dim_names} return blocks, array_dims diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index eb37552f..620b7eba 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -204,7 +204,7 @@ def _validate_sum_call(call: ast.Call, component: "ComponentBase", expr: str) -> ) -class DimDef(BaseModel): +class Dim(BaseModel): """A named dimension, either backed by a field or derived from an expression.""" field: str | None = None # name of the field that provides this dimension @@ -212,9 +212,9 @@ class DimDef(BaseModel): scope: Literal["component", "model", "simulation"] = "component" @model_validator(mode="after") - def _check_exclusive(self) -> "DimDef": + def _check_exclusive(self) -> "Dim": if (self.field is None) == (self.expr is None): - raise ValueError("DimDef must have exactly one of 'field' or 'expr'") + raise ValueError("Dim must have exactly one of 'field' or 'expr'") return self @property @@ -334,7 +334,7 @@ class ComponentBase(BaseModel): blocks: dict[str, Block] | None = None parent: str | list[str] | None = None schema_version: str | None = None - dims: dict[str, DimDef] | None = None + dims: dict[str, Dim] | None = None class Simulation(ComponentBase): From 911c77d69f6d8448e0504f4b421ee3a9e9fe5cc3 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 12:00:07 -0700 Subject: [PATCH 24/29] various fixes --- .github/workflows/ci.yml | 9 +- autotest/dfn/conftest.py | 16 ++++ autotest/dfn/test_dfn.py | 104 +++----------------- autotest/test_dfn2toml.py | 83 ++++++++++++++++ docs/md/dfnspec.md | 163 ++++++++++++-------------------- modflow_devtools/dfns/schema.py | 98 +++++++++++++++++-- 6 files changed, 271 insertions(+), 202 deletions(-) create mode 100644 autotest/dfn/conftest.py create mode 100644 autotest/test_dfn2toml.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 926dead4..8831d690 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,10 +51,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: "**/pyproject.toml" @@ -88,6 +88,8 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 + with: + path: modflow-devtools - name: Checkout modflow6 for DFN autodiscovery uses: actions/checkout@v4 @@ -96,10 +98,11 @@ jobs: path: modflow6 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: "**/pyproject.toml" python-version: ${{ matrix.python }} + working-directory: modflow-devtools - name: Setup Fortran if: runner.os != 'Windows' diff --git a/autotest/dfn/conftest.py b/autotest/dfn/conftest.py new file mode 100644 index 00000000..b49e5d71 --- /dev/null +++ b/autotest/dfn/conftest.py @@ -0,0 +1,16 @@ +import pytest + +from modflow_devtools.dfn import fetch_dfns + +MF6_OWNER = "MODFLOW-ORG" +MF6_REPO = "modflow6" +MF6_REF = "develop" + + +@pytest.fixture(scope="module") +def dfn_dir(module_tmpdir): + pytest.importorskip("boltons") + path = module_tmpdir / "dfn" + path.mkdir() + fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, path, verbose=True) + return path diff --git a/autotest/dfn/test_dfn.py b/autotest/dfn/test_dfn.py index b10e493a..2bf363c4 100644 --- a/autotest/dfn/test_dfn.py +++ b/autotest/dfn/test_dfn.py @@ -1,101 +1,23 @@ -import tempfile -from pathlib import Path - -from modflow_devtools.dfn import Dfn, fetch_dfns -from modflow_devtools.dfn2toml import convert +from modflow_devtools.dfn import Dfn from modflow_devtools.markers import requires_pkg -MF6_OWNER = "MODFLOW-ORG" -MF6_REPO = "modflow6" -MF6_REF = "develop" - -_DFN_DIR: Path | None = None -_TOML_V1_DIR: Path | None = None -_TOML_V1_1_DIR: Path | None = None -_TMPDIR: tempfile.TemporaryDirectory | None = None - - -def _ensure_dfns() -> Path: - global _DFN_DIR, _TMPDIR - if _DFN_DIR is None: - _TMPDIR = tempfile.TemporaryDirectory() - _DFN_DIR = Path(_TMPDIR.name) / "dfn" - _DFN_DIR.mkdir() - fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, _DFN_DIR, verbose=True) - return _DFN_DIR - - -def _ensure_toml_v1() -> Path: - global _TOML_V1_DIR - dfn_dir = _ensure_dfns() - if _TOML_V1_DIR is None: - _TOML_V1_DIR = dfn_dir / "toml" - convert(dfn_dir, _TOML_V1_DIR, schema_version="1") - return _TOML_V1_DIR - - -def _ensure_toml_v1_1() -> Path: - global _TOML_V1_1_DIR - dfn_dir = _ensure_dfns() - if _TOML_V1_1_DIR is None: - _TOML_V1_1_DIR = dfn_dir / "toml-v1_1" - convert(dfn_dir, _TOML_V1_1_DIR, schema_version="1.1") - return _TOML_V1_1_DIR - - -def pytest_generate_tests(metafunc): - if "dfn_name" in metafunc.fixturenames: - dfn_dir = _ensure_dfns() - dfn_names = [p.stem for p in dfn_dir.glob("*.dfn") if p.stem not in ("common", "flopy")] - metafunc.parametrize("dfn_name", dfn_names, ids=dfn_names) - - if "toml_name" in metafunc.fixturenames: - toml_dir = _ensure_toml_v1() - toml_names = [p.stem for p in toml_dir.glob("*.toml")] - metafunc.parametrize("toml_name", toml_names, ids=toml_names) - - if "toml_v1_1_name" in metafunc.fixturenames: - toml_dir = _ensure_toml_v1_1() - toml_names = [p.stem for p in toml_dir.glob("*.toml")] - metafunc.parametrize("toml_v1_1_name", toml_names, ids=toml_names) - @requires_pkg("boltons") -def test_load_v1(dfn_name): - dfn_dir = _ensure_dfns() - with ( - (dfn_dir / "common.dfn").open() as common_file, - (dfn_dir / f"{dfn_name}.dfn").open() as dfn_file, - ): - common, _ = Dfn._load_v1_flat(common_file) - dfn = Dfn.load(dfn_file, name=dfn_name, common=common) +def test_load_v1(dfn_dir): + common = {} + common_path = dfn_dir / "common.dfn" + if common_path.exists(): + with common_path.open() as f: + common, _ = Dfn._load_v1_flat(f) + names = [p.stem for p in dfn_dir.glob("*.dfn") if p.stem not in ("common", "flopy")] + assert names + for name in names: + with (dfn_dir / f"{name}.dfn").open() as f: + dfn = Dfn.load(f, name=name, common=common) assert any(dfn) @requires_pkg("boltons") -def test_load_v2(toml_name): - toml_dir = _ensure_toml_v1() - with (toml_dir / f"{toml_name}.toml").open(mode="rb") as toml_file: - toml = Dfn.load(toml_file, name=toml_name, version=2) - assert any(toml) - - -@requires_pkg("boltons") -def test_load_all(): - dfn_dir = _ensure_dfns() +def test_load_all(dfn_dir): dfns = Dfn.load_all(dfn_dir) assert any(dfns) - - -@requires_pkg("boltons") -def test_convert_v1_1(toml_v1_1_name): - try: - import tomllib - except ImportError: - import tomli as tomllib # type: ignore[no-redef] - - toml_dir = _ensure_toml_v1_1() - with (toml_dir / f"{toml_v1_1_name}.toml").open("rb") as f: - data = tomllib.load(f) - assert data["name"] == toml_v1_1_name - assert data["schema_version"] == "1.1" diff --git a/autotest/test_dfn2toml.py b/autotest/test_dfn2toml.py new file mode 100644 index 00000000..626c047f --- /dev/null +++ b/autotest/test_dfn2toml.py @@ -0,0 +1,83 @@ +import tomllib + +import pytest + +from modflow_devtools.dfn import Dfn, fetch_dfns +from modflow_devtools.dfn2toml import convert +from modflow_devtools.markers import requires_pkg + +MF6_OWNER = "MODFLOW-ORG" +MF6_REPO = "modflow6" +MF6_REF = "develop" + + +@pytest.fixture(scope="module") +def dfn_dir(module_tmpdir): + pytest.importorskip("boltons") + path = module_tmpdir / "dfn" + path.mkdir() + fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, path, verbose=True) + return path + + +@pytest.fixture(scope="module") +def toml_v1_dir(dfn_dir, module_tmpdir): + out = module_tmpdir / "toml-v1" + convert(dfn_dir, out, schema_version="1") + return out + + +@pytest.fixture(scope="module") +def toml_v1_1_dir(dfn_dir, module_tmpdir): + out = module_tmpdir / "toml-v1.1" + convert(dfn_dir, out, schema_version="1.1") + return out + + +@pytest.fixture(scope="module") +def toml_v2_dir(dfn_dir, module_tmpdir): + out = module_tmpdir / "toml-v2" + convert(dfn_dir, out, schema_version="2") + return out + + +@requires_pkg("boltons") +def test_convert_v1(toml_v1_dir): + tomls = list(toml_v1_dir.glob("*.toml")) + assert tomls + for p in tomls: + with p.open("rb") as f: + data = tomllib.load(f) + assert data["name"] == p.stem + assert data["schema_version"] == "1" + + +@requires_pkg("boltons") +def test_convert_v1_roundtrip(toml_v1_dir): + """Verify Dfn.load can read v1-schema TOML files.""" + for p in toml_v1_dir.glob("*.toml"): + with p.open("rb") as f: + dfn = Dfn.load(f, name=p.stem, version=2) + assert any(dfn) + + +@requires_pkg("boltons") +def test_convert_v1_1(toml_v1_1_dir): + tomls = list(toml_v1_1_dir.glob("*.toml")) + assert tomls + for p in tomls: + with p.open("rb") as f: + data = tomllib.load(f) + assert data["name"] == p.stem + assert data["schema_version"] == "1.1" + + +@requires_pkg("boltons") +def test_convert_v2(toml_v2_dir): + tomls = list(toml_v2_dir.glob("*.toml")) + assert tomls + for p in tomls: + with p.open("rb") as f: + data = tomllib.load(f) + assert data["name"] == p.stem + assert data["schema_version"] == "2" diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 642dde02..da9e8c6d 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -10,6 +10,7 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [`blocks`](#blocks) - [`parent`](#parent) - [`schema_version`](#schema_version) + - [`dims`](#dims) - [Component types](#component-types) - [Simulation](#simulation) - [Model](#model) @@ -74,11 +75,8 @@ This document describes the MODFLOW 6 component definition (DFN) system. This sy - [List](#list) - [Type-specific attributes](#type-specific-attributes-9) - [`item`](#item) - - [Array dimensions](#array-dimensions) - - [Derived dimensions](#derived-dimensions) - - [Row-level column lookups](#row-level-column-lookups) - - [Intra-record sibling references](#intra-record-sibling-references) - - [Scope and resolution](#scope-and-resolution) + - [Dimensions](#dimensions) + - [Dimension sources](#dimension-sources) - [Dimension scope](#dimension-scope) - [Primary/foreign keys](#primaryforeign-keys) - [Examples](#examples) @@ -140,6 +138,10 @@ Parent relationships are defined bottom-up with attribute `parent`: `string | null (default: null)`. The version of the DFN schema. Optional but recommended. When multiple components are loaded together into a `DfnSpec`, all non-null `schema_version` values must agree; mixed versions are a validation error. +#### `dims` + +`{string: Dim} (default: {})`. Named dimensions available for use in array and list shape expressions. Each entry is either field-backed (`field`: the name of an integer or self-sizing array field in this component) or derived (`expr`: an arithmetic expression over other dims), plus a `scope` that controls visibility to other components. See [Dimensions](#dimensions). + ### Component types Three component types can be distinguished: simulation, model, and package. @@ -388,7 +390,9 @@ Type `array`. Arrays are not proper composites. An array does not have an item subfield as does a list. Instead, it has a `dtype` attribute identifying its scalar element type. An array may not contain composite elements; `dtype` must be a scalar type. -An array is **self-sizing** when its `shape` is empty (`[]`). A self-sizing array is read inline by the parser: it consumes tokens until the end of the current line, dynamically determining its own length. Its element count may serve as a dimension for other arrays (see `dimension` below). The only invalid position for a self-sizing array is as a non-rightmost subfield of a record, where subsequent fields on the same line would be unreadable. Arrays with a declared shape are parsed to exactly that many elements. +A 1D array may have absent or empty `shape`, indicating no constraint on its size, in which case it is called **self-sizing**. Self-sizing arrays are parsed by MF6 dynamically at runtime. The size of a self-sizing array may serve as a dimension for other arrays (see below). + +A 1D array appearing as a subfield of a record is called an **inline array**. Inline arrays with a declared shape are self-explanatory. An inline array may only be self-sizing if it is the right-most subfield of the record; in this case the record is essentially a variadic tuple. ##### Type-specific attributes @@ -398,13 +402,7 @@ An array is **self-sizing** when its `shape` is empty (`[]`). A self-sizing arra ###### `shape` -`[string] (default: [])`. The array's shape, as a list of shape expressions. An empty list means the array is **self-sizing** (see above). There is one constraint on self-sizing arrays: they may not appear as a non-rightmost subfield of a record, because subsequent fields on the same line would be unreadable. In all other positions — top-level in a block, or rightmost in a record — a self-sizing array is valid. Parsing rules by position: - -- **Top-level** (a direct field of a block, not inside a record): read as inline tokens on the block line. `shape` may be empty (self-sizing) or declared. -- **Inline, not rightmost** (a subfield of a record with at least one subsequent field): `shape` must be declared and non-empty; the size must be determinable from already-parsed context (a global dim or a preceding sibling field). -- **Inline, rightmost** (the last subfield of a record): `shape` may be empty (self-sizing) or declared. - -Each declared shape expression is either a global dimension name (explicit or derived; see [Array dimensions](#array-dimensions)) or a row-level column lookup (see [Row-level column lookups](#row-level-column-lookups)). The latter form is only valid when the array is a subfield of a record. Any shape expression may additionally carry an advisory **bound annotation** prefix (`<`, `>`, `<=`, or `>=`); the bound is not enforced by the MF6 parser. +`[string] (default: [])`. The array's shape, as a list of shape expressions, one per dimension. An empty list means the array is 1-dimensional and **self-sizing** (see above). ###### `time_series` @@ -446,11 +444,15 @@ Type `list`. Collection type. Unlimited but for one rule: a list may not contain ###### `item` -`Record | Union`. Subfield (item type), required. +`record | union`. Item type, required. + +###### `shape` + +`[string] (default: [])`. The list's shape, as a list of shape expressions. May have at most one element, as lists are necessarily 1-dimensional. -### Array dimensions +### Dimensions -Dimensions are declared at the component level via the `dims` map, not on individual fields. Each entry in `dims` consists of a source (a field or an expression combining other dimension fields) and a scope: +Dimensions may be declared by a component with a `dims` map. Each entry in `dims` consists of a **source** (a field or an expression combining other dimension fields) and a **scope**: ```yaml dims: @@ -465,23 +467,24 @@ dims: scope: simulation ``` -The `field` and `expr` attributes are mutually exclusive, distinguishing explicit field-backed dimensions from derived dimensions: -- `field`: the name of an `integer` or array field in this component that provides the dimension value -- `expr`: an arithmetic expression that derives the dimension from other known dims +Dimensions may be used in the `shape` expression of `list` and `array` fields. + +#### Dimension sources -The `scope` (see [Scope and resolution](#scope-and-resolution)) controls which other components can see the dimension. +The `field` and `expr` attributes define dimension sources. These attributes are mutually exclusive, distinguishing **explicit dimensions** from **derived dimensions**. -Integer fields or self-sizing `array` fields (those with empty or absent `shape`) may serve as explicit dimensions. An integer field directly indicates the dimension size, whereas a self-sizing array indicate "one element per item in this array." +Explicit dimensions are most straightforwardly defined with an `integer` field, directly indicating the size of the dimension. Self-sizing `array` fields may also may serve as explicit dimension sources; a self-sizing array dimension is effectively a dynamic dimension size, indicating "the same size as this array". -A dimension with an `expr` attribute rather than a `field` is a derived dimension. Shape expressions use Python-like syntax and may contain several kinds of reference: +A dimension with an `expr` attribute rather than `field` is a derived dimension. Shape expressions use Python-like syntax and may contain several kinds of reference, resolved in the order presented below: -- **Dimension reference** (`^[A-Za-z_]\w*$`): names a dimension either resolved locally or inherited from another component. -- **Record subfield reference**: names a sibling `integer` subfield in the same record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. -- **List column reference** (`block.list.column(fk_field)`): names a subfield of a record which is the item type of a regular (i.e. tabular) list. +- **Local dimension**: an explicit or derived dimension in this component, resolved in dependency order. +- **Inherited dimension**: a dimension inherited from another component, per scoping rules (see below). +- **Record subfield**: a sibling `integer` subfield in the same record. Makes the record a variadic tuple whose width varies per row. Valid only when the array is a subfield of a record. +- **List column**: a subfield of a record which is the item type of a regular (i.e. tabular) list, in this component or another. If in another component, the name must be fully qualified (see below). Valid only when the array is a subfield of a record. Shape expressions may also include simple integer arithmetic, e.g. `nlay + 1`, as well as constraints, e.g. `<`, `>`, `<=`, or `>=` and simple math functions like `sum()`. -Canonical examples of explicit and derived dimensions: +Canonical examples of dimensions: ```yaml dims: @@ -505,27 +508,35 @@ dims: total_lake_connections: {expr: "sum(packagedata.nlakeconn)", scope: component} ``` -#### Inline arrays +An inline array may have its size determined by an integer subfield in the same record, or if the record it is within is the item type of a list, by a column in another list. -An array appearing as a subfield of a record is called an **inline array**. An inline array may have its size determined by an integer subfield in the same record, or by a column in another list, . The shape expression form for this is: +If a record contains an `integer` field followed by an `array` whose shape expression names that field, the record is self-describing: it carries its own sizing information. Inline array dimensions of this kind need not be declared at the component level. For instance: -``` -block.column(fk_field) +```yaml +connectiondata: + type: list + item: + type: record + fields: + icno: + type: integer + fk: "packagedata.icno" + ncvert: + type: integer + icvert: + type: array + dtype: integer + shape: ["ncvert"] ``` -where: -- `block` is the name of a list block in the same component -- `column` is an integer column in that list's item record -- `fk_field` is a sibling field in the same record whose `fk` attribute resolves to a PK in `block` +If an inline array appears in a record which is the item type of a list, the array's size may be specified by a column in another regular list. This is a form of primary-/foreign-key relation; see [Primary/foreign keys](#primaryforeign-keys). The syntax for this form is `[component.]block.column(fk_field)`. If the list is in the same component, the dimension need not be declared in `dims`. -This notation is consistent with FK path conventions (`block.field` for within-component references). The parenthetical `(fk_field)` serves as the row selector, distinguishing this form from a derived dimension expression over the same path. Cross-component references extend the path naturally to `component.block.column(fk_field)`. +- `component`: the component name. Only required if in a different component. +- `block`: the name of the block containing the list. In all current cases the list field has the same name as its containing block, so this also serves as the list name. +- `column`: an integer subfield in the list's record item type. +- `fk_field`: an integer subfield in the referring array's record whose `fk` attribute resolves to a `pk` subfield in the dimension-providing record. -Validation rules: -- `fk_field` must be a sibling field in the same enclosing record -- `fk_field` must have `fk` set, and its `fk` attribute's block portion must match `block` -- `block`'s list item record must have exactly one `pk: true` field -- `column` must exist in `block`'s item record and be of type `integer` -- This form is only valid when the array is a subfield of a record; it is a schema error on a top-level array field +The parenthetical `(fk_field)` serves as the row selector, distinguishing this form from a derived dimension expression over the same path, e.g. `[component.]block.column` The canonical example of this is `gwf-sfr.connectiondata.ic`, an array field whose length varies per reach according to the `ncon` column in `packagedata`: @@ -556,74 +567,22 @@ connectiondata: shape: ["packagedata.ncon(ifno)"] ``` -`packagedata.ncon(ifno)` means: follow `ifno`'s FK to identify the `packagedata` row, then read `ncon` from it. Each `connectiondata` row has a different number of `ic` values. The `ic` array is read inline on the same line as the rest of the record, making the record a variadic tuple. - -#### Intra-record sibling references - -A record may also be a variadic tuple without any FK-linked list. If a record contains an `integer` field followed by an `array` whose shape names that field, the record is self-describing: it carries its own count. The `array`'s shape element is a plain identifier that resolves to the sibling field, not to a global dim. - -```yaml -connectiondata: - type: list - item: - type: record - fields: - icno: - type: integer - fk: "packagedata.icno" - ncvert: - type: integer - icvert: - type: array - dtype: integer - shape: ["ncvert"] -``` - -`ncvert` is not a globally declared dim — it is a sibling field in the same record row. Each row supplies `ncvert` first, then `ncvert` vertex indices inline. This is structurally distinct from the row-level column lookup: no FK relationship is implied, and no separate list is consulted. The count is simply read from the same line. - -Validation rules: -- Valid only when the array is a subfield of a record (not a top-level block field). -- The sibling field must be an `integer`. No special annotation is required on the sibling. -- Resolution order: the scope chain is tried first; sibling resolution is only the fallback when the identifier does not resolve globally. - -#### Bound-annotated shape expressions - -A dim reference (either a global dim or an intra-record sibling) may be prefixed with a relational operator (`<`, `>`, `<=`, `>=`) to express an advisory bound on the array's length, e.g. ` "dict[str, Field]": class List(FieldBase): type: Literal["list"] = PydanticField(default="list", frozen=True) item: "Record | Union" + shape: list[str] = [] + + @model_validator(mode="after") + def _check_shape_length(self) -> "List": + if len(self.shape) > 1: + raise ValueError( + f"List {self.name!r}: shape must have at most one element " + f"(lists are 1-dimensional), got {self.shape!r}" + ) + return self @property def children(self) -> "dict[str, Field]": @@ -358,7 +368,7 @@ class Package(ComponentBase): ] _DIM_RE = re.compile(r"^[A-Za-z_]\w*$") -_LOOKUP_RE = re.compile(r"^(\w+)\.(\w+)\((\w+)\)$") +_LOOKUP_RE = re.compile(r"^(?:([\w-]+)\.)?(\w+)\.(\w+)\((\w+)\)$") _BOUND_RE = re.compile(r"^[<>]=?") _ARITH_RE = re.compile(r"^([A-Za-z_]\w*)\s*[+-]\s*\d+$") @@ -380,6 +390,7 @@ def _validate_shape_element( component: "ComponentBase", enclosing_record: "Record | None", known_dims: set[str], + spec: "Dfns | None" = None, ) -> None: """ Validate one element of an Array.shape list. @@ -427,7 +438,7 @@ def _validate_shape_element( ) if m := _LOOKUP_RE.fullmatch(element): - block_name, col_name, fk_field_name = m.groups() + component_ref, block_name, col_name, fk_field_name = m.groups() # Check 5: array must be a subfield of a record, not a top-level block field if enclosing_record is None: @@ -436,12 +447,29 @@ def _validate_shape_element( f"row-level lookup but the array is not inside a record" ) - # Check 1: block_name must identify a list block in this component - list_field = _find_list_in_block(component, block_name) + # Resolve target component (cross-component reference or local) + if component_ref is not None: + if spec is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"cross-component reference requires a Dfns spec" + ) + target = spec.components.get(component_ref) + if target is None: + raise ValueError( + f"Array {array_field.name!r} shape element {element!r}: " + f"component {component_ref!r} not found in spec" + ) + else: + target = component # type: ignore + + # Check 1: block_name must identify a list block in the target component + list_field = _find_list_in_block(target, block_name) # type: ignore if list_field is None: + where = f"component {component_ref!r}" if component_ref else "this component" raise ValueError( f"Array {array_field.name!r} shape element {element!r}: " - f"{block_name!r} is not a list block in this component" + f"{block_name!r} is not a list block in {where}" ) # Check 2: col_name must be an Integer field in the list's item record @@ -504,6 +532,58 @@ def _validate_shape_element( ) +def _validate_list_shape_element( + element: str, + list_field: "List", + known_dims: set[str], +) -> None: + """ + Validate one element of a List.shape. + + Valid forms are a strict subset of array shape forms — no row-level lookup + and no intra-record sibling reference, since lists are not inside records: + - Plain dim reference + - Bound-annotated dim reference (<, >, <=, >=) + - Arithmetic offset (dim [+-] integer) + """ + if bound_m := _BOUND_RE.match(element): + core = element[bound_m.end() :] + if not _DIM_RE.fullmatch(core): + raise ValueError( + f"List {list_field.name!r} has invalid shape element {element!r}: " + f"must be a plain identifier after the bound operator" + ) + if core not in known_dims: + raise ValueError( + f"List {list_field.name!r} shape element {element!r}: " + f"{core!r} does not resolve to a known dim" + ) + return + + if _DIM_RE.fullmatch(element): + if element not in known_dims: + raise ValueError( + f"List {list_field.name!r} shape element {element!r} " + f"does not resolve to a known dim" + ) + return + + if m := _ARITH_RE.fullmatch(element): + dim_name = m.group(1) + if dim_name not in known_dims: + raise ValueError( + f"List {list_field.name!r} shape element {element!r}: " + f"{dim_name!r} does not resolve to a known dim" + ) + return + + raise ValueError( + f"List {list_field.name!r} has invalid shape element {element!r}: " + f"must be a dim reference (^[A-Za-z_]\\w*$), an arithmetic offset " + f"(dim [+-] integer), or a bound-annotated dim (/>=dim)" + ) + + def _validate_fk_fields(component: "ComponentBase", spec: "Dfns") -> None: """ For every Integer/String field with fk or fk_ref set, validate structural @@ -574,6 +654,10 @@ def _validate_array_shapes( known_dims = spec.dims(component_name) + def _check_list(lst: "List") -> None: + for elem in lst.shape: + _validate_list_shape_element(elem, lst, known_dims) + def _check_array(arr: "Array", enclosing: "Record | None") -> None: if not arr.shape: # Self-sizing (shape=[]) is valid at the top level and as the rightmost @@ -588,7 +672,7 @@ def _check_array(arr: "Array", enclosing: "Record | None") -> None: ) return # self-sizing: nothing to validate for elem in arr.shape: - _validate_shape_element(elem, arr, component, enclosing, known_dims) + _validate_shape_element(elem, arr, component, enclosing, known_dims, spec) for block in component.blocks.values(): for field in block.fields.values(): @@ -601,6 +685,8 @@ def _check_array(arr: "Array", enclosing: "Record | None") -> None: _check_array(subfield, field) elif isinstance(field, List): + if field.shape: + _check_list(field) item = field.item if isinstance(item, Record): for subfield in item.fields.values(): From efb0cd5aae438b43b2d6857a7c98fcd12b347f3a Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 12:29:29 -0700 Subject: [PATCH 25/29] support yaml and json --- autotest/test_dfn2toml.py | 83 ---------------- autotest/test_dfnmap.py | 104 ++++++++++++++++++++ docs/md/dfns.md | 16 +-- modflow_devtools/dfn/schema.py | 67 +++++++++---- modflow_devtools/{dfn2toml.py => dfnmap.py} | 82 ++++++++++----- modflow_devtools/dfns/mapper.py | 68 ++++++++++++- modflow_devtools/dfns/schema.py | 16 +++ pyproject.toml | 4 + 8 files changed, 304 insertions(+), 136 deletions(-) delete mode 100644 autotest/test_dfn2toml.py create mode 100644 autotest/test_dfnmap.py rename modflow_devtools/{dfn2toml.py => dfnmap.py} (61%) diff --git a/autotest/test_dfn2toml.py b/autotest/test_dfn2toml.py deleted file mode 100644 index 626c047f..00000000 --- a/autotest/test_dfn2toml.py +++ /dev/null @@ -1,83 +0,0 @@ -import tomllib - -import pytest - -from modflow_devtools.dfn import Dfn, fetch_dfns -from modflow_devtools.dfn2toml import convert -from modflow_devtools.markers import requires_pkg - -MF6_OWNER = "MODFLOW-ORG" -MF6_REPO = "modflow6" -MF6_REF = "develop" - - -@pytest.fixture(scope="module") -def dfn_dir(module_tmpdir): - pytest.importorskip("boltons") - path = module_tmpdir / "dfn" - path.mkdir() - fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, path, verbose=True) - return path - - -@pytest.fixture(scope="module") -def toml_v1_dir(dfn_dir, module_tmpdir): - out = module_tmpdir / "toml-v1" - convert(dfn_dir, out, schema_version="1") - return out - - -@pytest.fixture(scope="module") -def toml_v1_1_dir(dfn_dir, module_tmpdir): - out = module_tmpdir / "toml-v1.1" - convert(dfn_dir, out, schema_version="1.1") - return out - - -@pytest.fixture(scope="module") -def toml_v2_dir(dfn_dir, module_tmpdir): - out = module_tmpdir / "toml-v2" - convert(dfn_dir, out, schema_version="2") - return out - - -@requires_pkg("boltons") -def test_convert_v1(toml_v1_dir): - tomls = list(toml_v1_dir.glob("*.toml")) - assert tomls - for p in tomls: - with p.open("rb") as f: - data = tomllib.load(f) - assert data["name"] == p.stem - assert data["schema_version"] == "1" - - -@requires_pkg("boltons") -def test_convert_v1_roundtrip(toml_v1_dir): - """Verify Dfn.load can read v1-schema TOML files.""" - for p in toml_v1_dir.glob("*.toml"): - with p.open("rb") as f: - dfn = Dfn.load(f, name=p.stem, version=2) - assert any(dfn) - - -@requires_pkg("boltons") -def test_convert_v1_1(toml_v1_1_dir): - tomls = list(toml_v1_1_dir.glob("*.toml")) - assert tomls - for p in tomls: - with p.open("rb") as f: - data = tomllib.load(f) - assert data["name"] == p.stem - assert data["schema_version"] == "1.1" - - -@requires_pkg("boltons") -def test_convert_v2(toml_v2_dir): - tomls = list(toml_v2_dir.glob("*.toml")) - assert tomls - for p in tomls: - with p.open("rb") as f: - data = tomllib.load(f) - assert data["name"] == p.stem - assert data["schema_version"] == "2" diff --git a/autotest/test_dfnmap.py b/autotest/test_dfnmap.py new file mode 100644 index 00000000..582138d0 --- /dev/null +++ b/autotest/test_dfnmap.py @@ -0,0 +1,104 @@ +import json +import tomllib +from pathlib import Path + +import pytest +import yaml + +from modflow_devtools.dfn import Dfn, fetch_dfns +from modflow_devtools.dfnmap import migrate +from modflow_devtools.markers import requires_pkg + +FORMATS = ["yaml", "toml", "json"] +MF6_OWNER = "MODFLOW-ORG" +MF6_REPO = "modflow6" +MF6_REF = "develop" + + +def _load(path: Path, fmt: str) -> dict: + if fmt == "toml": + with path.open("rb") as f: + return tomllib.load(f) + elif fmt == "json": + with path.open() as f: + return json.load(f) + else: + with path.open() as f: + return yaml.safe_load(f) + + +@pytest.fixture(scope="module") +def dfn_dir(module_tmpdir): + pytest.importorskip("boltons") + path = module_tmpdir / "dfn" + path.mkdir() + fetch_dfns(MF6_OWNER, MF6_REPO, MF6_REF, path, verbose=True) + return path + + +@pytest.fixture(scope="module", params=FORMATS) +def converted_v1(request, dfn_dir, module_tmpdir): + fmt = request.param + out = module_tmpdir / f"v1-{fmt}" + migrate(dfn_dir, out, schema_version="1", fmt=fmt) + return out, fmt + + +@pytest.fixture(scope="module", params=FORMATS) +def converted_v1_1(request, dfn_dir, module_tmpdir): + fmt = request.param + out = module_tmpdir / f"v1.1-{fmt}" + migrate(dfn_dir, out, schema_version="1.1", fmt=fmt) + return out, fmt + + +@pytest.fixture(scope="module", params=FORMATS) +def converted_v2(request, dfn_dir, module_tmpdir): + fmt = request.param + out = module_tmpdir / f"v2-{fmt}" + migrate(dfn_dir, out, schema_version="2", fmt=fmt) + return out, fmt + + +@requires_pkg("boltons") +def test_convert_v1(converted_v1): + out, fmt = converted_v1 + files = list(out.glob(f"*.{fmt}")) + assert files + for p in files: + data = _load(p, fmt) + assert data["name"] == p.stem + assert data["schema_version"] == "1" + + +@requires_pkg("boltons") +def test_convert_v1_1(converted_v1_1): + out, fmt = converted_v1_1 + files = list(out.glob(f"*.{fmt}")) + assert files + for p in files: + data = _load(p, fmt) + assert data["name"] == p.stem + assert data["schema_version"] == "1.1" + + +@requires_pkg("boltons") +def test_convert_v2(converted_v2): + out, fmt = converted_v2 + files = list(out.glob(f"*.{fmt}")) + assert files + for p in files: + data = _load(p, fmt) + assert data["name"] == p.stem + assert data["schema_version"] == "2" + + +@requires_pkg("boltons") +def test_roundtrip(converted_v2): + """Verify Dfn.load can read v2-schema files in any format.""" + out, fmt = converted_v2 + mode = "rb" if fmt == "toml" else "r" + for p in out.glob(f"*.{fmt}"): + with p.open(mode) as f: + dfn = Dfn.load(f, name=p.stem, version=fmt) + assert any(dfn) diff --git a/docs/md/dfns.md b/docs/md/dfns.md index 9d266553..f80d944c 100644 --- a/docs/md/dfns.md +++ b/docs/md/dfns.md @@ -23,19 +23,19 @@ Downloads all `.dfn` files for the specified MODFLOW 6 release into the given ou ### Converting to TOML -The `dfn` dependency group is required for the TOML conversion tool: +The `dfn` dependency group is required for the conversion tool: ```shell pip install modflow-devtools[dfn] ``` -To convert legacy `.dfn` files to TOML: +To convert legacy `.dfn` files (default output format is YAML): ```shell -python -m modflow_devtools.dfn2toml -i -o +python -m modflow_devtools.dfnmap -i -o ``` -The tool may also be used on individual files. To validate legacy format files, use the `--validate` flag. +Use `--format` / `-f` to select `yaml` (default), `toml`, or `json`. The tool may also be used on individual files. --- @@ -56,14 +56,18 @@ These are two separate concerns. **File format** is the serialization: - **Legacy DFN format** (`.dfn`): flat text with comments demarcating blocks, used by MODFLOW 6 releases. -- **TOML format** (`.toml`): per-component TOML documents, produced by the `dfn2toml` conversion tool. +- **TOML format** (`.toml`): per-component TOML documents. +- **YAML format** (`.yaml`): per-component YAML documents. +- **JSON format** (`.json`): per-component JSON documents. + +TOML, YAML, and JSON files are produced by the `dfnmap` conversion tool. **Schema version** describes the structure and semantics of the content: - **v1 schema**: the original structure embedded in legacy `.dfn` files. Mixes structural definitions with input format details (e.g., `in_record`, `tagged`). - **v2 schema**: a cleaner, hierarchical representation. Each component has explicitly typed, nested fields; blocks and records are first-class objects; structural specification is separated from input format concerns. -`modflow_devtools.dfns` always works with v2 schema objects internally. When loading a directory of `.dfn` files, they are parsed as v1 and automatically mapped to v2. TOML files carry v2 content directly and are loaded without mapping. Both file formats are supported by `Dfns.load()`. +`modflow_devtools.dfns` always works with v2 schema objects internally. When loading a directory of `.dfn` files, they are parsed as v1 and automatically mapped to v2. TOML, YAML, and JSON files carry v2 content directly and are loaded without mapping. All file formats are supported by `Dfns.load()`. ### Core classes diff --git a/modflow_devtools/dfn/schema.py b/modflow_devtools/dfn/schema.py index d428695d..5ce62af2 100644 --- a/modflow_devtools/dfn/schema.py +++ b/modflow_devtools/dfn/schema.py @@ -98,7 +98,7 @@ def block_sort_key(item: tuple[str, Any]) -> int: """DFN format version number.""" -DfnFormat = Literal["dfn", "toml"] +DfnFormat = Literal["dfn", "toml", "yaml", "json"] """DFN serialization format.""" @@ -612,8 +612,19 @@ def _subcomponents() -> list[str] | None: ) @classmethod # type: ignore[misc] - def _load_v2(cls, f, name) -> "Dfn": - data = tomli.load(f) + def _load_v2(cls, f, name, fmt: str = "toml") -> "Dfn": + if fmt == "toml": + data = tomli.load(f) + elif fmt == "json": + import json + + data = json.load(f) + elif fmt == "yaml": + import yaml + + data = yaml.safe_load(f) + else: + raise ValueError(f"Unsupported format: {fmt!r}") if name and name != data.get("name", None): raise ValueError(f"Name mismatch, expected {name}") return cls(**data) @@ -633,9 +644,15 @@ def load( if version in ["dfn", 1]: return cls._load_v1(f, name, **kwargs) elif version in ["toml", 2]: - return cls._load_v2(f, name) + return cls._load_v2(f, name, fmt="toml") + elif version == "yaml": + return cls._load_v2(f, name, fmt="yaml") + elif version == "json": + return cls._load_v2(f, name, fmt="json") else: - raise ValueError(f"Unsupported version, expected one of {version.__args__}") + raise ValueError( + f"Unsupported version {version!r}, expected one of: 'dfn', 'toml', 'yaml', 'json', 1, 2" + ) @staticmethod # type: ignore[misc] def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: @@ -645,20 +662,26 @@ def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: warn("load_all() argument 'version' is deprecated and ignored") dfns: Dfns = {} - - dfn_paths: list[Path] = [ - p for p in dfndir.glob("*.dfn") if p.stem not in ["common", "flopy"] - ] - toml_paths: list[Path] = [ - p for p in dfndir.glob("*.toml") if p.stem not in ["common", "flopy"] + dfndir = Path(dfndir) + _EXCLUDE = {"common", "flopy"} + + dfn_paths: list[Path] = [p for p in dfndir.glob("*.dfn") if p.stem not in _EXCLUDE] + toml_paths: list[Path] = [p for p in dfndir.glob("*.toml") if p.stem not in _EXCLUDE] + yaml_paths: list[Path] = [ + p + for ext in ("*.yaml", "*.yml") + for p in dfndir.glob(ext) + if p.stem not in _EXCLUDE ] + json_paths: list[Path] = [p for p in dfndir.glob("*.json") if p.stem not in _EXCLUDE] - if any(dfn_paths) and any(toml_paths): - raise ValueError("Directory contains both DFN and TOML definition files") - if not any(dfn_paths) and not any(toml_paths): + groups = [g for g in [dfn_paths, toml_paths, yaml_paths, json_paths] if g] + if len(groups) > 1: + raise ValueError("Directory contains definition files in multiple formats") + if not groups: raise ValueError("Directory contains no definition files") - if any(dfn_paths): + if dfn_paths: # load common fields common_path: Path | None = dfndir / "common.dfn" if not common_path.is_file(): @@ -681,10 +704,20 @@ def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: with path.open() as f: dfn = Dfn.load(f, name=path.stem, common=common, refs=refs) dfns[path.stem] = dfn - else: + elif toml_paths: for path in toml_paths: with path.open(mode="rb") as f: - dfn = Dfn.load(f, name=path.stem) + dfn = Dfn.load(f, name=path.stem, version="toml") + dfns[path.stem] = dfn + elif yaml_paths: + for path in yaml_paths: + with path.open() as f: + dfn = Dfn.load(f, name=path.stem, version="yaml") + dfns[path.stem] = dfn + elif json_paths: + for path in json_paths: + with path.open() as f: + dfn = Dfn.load(f, name=path.stem, version="json") dfns[path.stem] = dfn return dfns diff --git a/modflow_devtools/dfn2toml.py b/modflow_devtools/dfnmap.py similarity index 61% rename from modflow_devtools/dfn2toml.py rename to modflow_devtools/dfnmap.py index ba2e982d..f039a753 100644 --- a/modflow_devtools/dfn2toml.py +++ b/modflow_devtools/dfnmap.py @@ -1,23 +1,26 @@ -"""Convert MODFLOW 6 DFN files to TOML.""" +"""Map MODFLOW 6 DFN files to a new schema version and serialize to YAML, TOML, or JSON.""" import argparse +import json from os import PathLike from pathlib import Path -from typing import Any +from typing import Any, Literal -import tomli_w as tomli +import pyaml +import tomli_w from pydantic import BaseModel from modflow_devtools.dfn import schema as v1 from modflow_devtools.dfn.mapper import map as map_v1_1 from modflow_devtools.dfns.mapper import map as map_v2 +Format = Literal["yaml", "toml", "json"] + +_EXT: dict[str, str] = {"yaml": ".yaml", "toml": ".toml", "json": ".json"} -def _toml_safe(obj: Any) -> Any: - """ - Recursively coerce non-TOML-native types to containers - and primitives suitable for TOML serialization. - """ + +def _serialize_safe(obj: Any) -> Any: + """Recursively coerce non-native types to primitives suitable for serialization.""" if isinstance(obj, BaseModel): return obj.model_dump( @@ -26,19 +29,36 @@ def _toml_safe(obj: Any) -> Any: exclude_defaults=True, ) if isinstance(obj, dict): - return {k: _toml_safe(v) for k, v in obj.items() if v is not None} + return {k: _serialize_safe(v) for k, v in obj.items() if v is not None} if isinstance(obj, list): - return [_toml_safe(v) for v in obj] + return [_serialize_safe(v) for v in obj] if isinstance(obj, (str, int, float, bool)) or obj is None: return obj return str(obj) # Version → str, etc. +def _write(data: dict, path: Path, fmt: Format) -> None: + if fmt == "toml": + with path.open("wb") as f: + tomli_w.dump(data, f) + elif fmt == "json": + with path.open("w") as f: + json.dump(data, f, indent=2) + elif fmt == "yaml": + with path.open("w") as f: + pyaml.dump(data, f) + + # mypy: ignore-errors -def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str = "2") -> None: - """Migrate DFN files' schema version and convert to TOML. +def migrate( + inpath: str | PathLike, + outdir: str | PathLike, + schema_version: str = "2", + fmt: Format = "yaml", +) -> None: + """Migrate DFN files' schema version and serialize to the given format. Parameters ---------- @@ -48,10 +68,13 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str Output directory. schema_version : str, optional Target schema version: "1", "1.1", or "2". Default "2". + fmt : str, optional + Output format: "yaml", "toml", or "json". Default "yaml". """ inpath = Path(inpath).expanduser().absolute() outdir = Path(outdir).expanduser().absolute() outdir.mkdir(exist_ok=True, parents=True) + ext = _EXT[fmt] if inpath.is_file(): if inpath.name == "common.dfn": @@ -66,7 +89,7 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str dfn = v1.Dfn.load(f, name=inpath.stem, common=common) if schema_version == "1": - pass # nothing to do + pass elif schema_version == "1.1": dfn = map_v1_1(dfn) elif schema_version == "2": @@ -76,14 +99,12 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" ) - dfn_path = outdir / f"{inpath.stem}.toml" - with Path.open(dfn_path, "wb") as f: - tomli.dump(_toml_safe(dfn), f) + _write(_serialize_safe(dfn), outdir / f"{inpath.stem}{ext}", fmt) else: dfns = v1.load_all(inpath) if schema_version == "1": - pass # nothing to do + pass elif schema_version == "1.1": dfns = v1.to_flat(v1.to_tree(dfns)) dfns = {name: map_v1_1(dfn) for name, dfn in dfns.items()} @@ -95,27 +116,22 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str ) for dfn_name, dfn in dfns.items(): - dfn_path = outdir / f"{dfn_name}.toml" - with Path.open(dfn_path, "wb") as f: - tomli.dump(_toml_safe(dfn), f) - - -convert = migrate # backwards-compatible alias + _write(_serialize_safe(dfn), outdir / f"{dfn_name}{ext}", fmt) if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Migrate DFN files' schema version and convert to TOML.", + description="Migrate DFN files' schema version and serialize to YAML, TOML, or JSON.", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( - "--indir", + "--input", "-i", type=str, help="Input file or directory containing DFN files.", ) parser.add_argument( - "--outdir", + "--output", "-o", help="Output directory.", ) @@ -126,5 +142,17 @@ def migrate(inpath: str | PathLike, outdir: str | PathLike, schema_version: str choices=["1", "1.1", "2"], help="Target schema version (default: 2).", ) + parser.add_argument( + "--format", + "-f", + default="yaml", + choices=["yaml", "toml", "json"], + help="Output format (default: yaml).", + ) args = parser.parse_args() - migrate(indir=args.indir, outdir=args.outdir, schema_version=args.schema_version) + migrate( + inpath=args.input, + outdir=args.output, + schema_version=args.schema_version, + fmt=args.format, + ) diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index e2d88057..bee652e3 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -282,12 +282,12 @@ def _to_scalar() -> v2.Scalar: developmode=developmode, netcdf=netcdf, tagged=tagged, - valid=list(valid) if valid else None, + valid=valid.split() if isinstance(valid, str) and valid else (list(valid) if valid else None), case_sensitive=preserve_case, time_series=time_series, ) if _type == "integer": - v = [int(x) for x in valid] if valid else None + v = [int(x) for x in valid.split()] if isinstance(valid, str) and valid else ([int(x) for x in valid] if valid else None) return v2.Integer( name=_name, longname=longname, @@ -419,7 +419,69 @@ def _record_fields() -> dict: ) if _type.startswith("record"): - rec_fields = _record_fields() + subnames = (_type or "").split()[1:] + # Detect filerecord: a subfield named 'filein' or 'fileout' with type keyword + file_mode: str | None = None + for sname in subnames: + if sname in ("filein", "fileout"): + m = next( + (fi for fi in fields.values(multi=True) + if fi["name"] == sname and try_parse_bool(fi.get("in_record", False))), + None, + ) + if m and (m.get("type") or "").strip() == "keyword": + file_mode = sname + break + + if file_mode: + # Filerecord pattern: + # In v2: drop the mode keyword and the untagged path string; promote + # the tag keyword to a File field (tagged=True, name=tag keyword name). + # Find the untagged string (the path value) so we can skip it. + path_field_name: str | None = None + for sname in subnames: + if sname == file_mode: + continue + m_s = next( + (fi for fi in fields.values(multi=True) + if fi["name"] == sname and try_parse_bool(fi.get("in_record", False))), + None, + ) + if m_s and (m_s.get("type") or "").strip() == "string" and not _to_bool(m_s.get("tagged"), True): + path_field_name = sname + break + + rec_fields = {} + for rname in subnames: + if rname in (file_mode, path_field_name): + continue # drop mode keyword and path string + m = next( + (fi for fi in fields.values(multi=True) + if fi["name"] == rname + and try_parse_bool(fi.get("in_record", False)) + and not (fi.get("type") or "").startswith("record")), + None, + ) + if m is None: + continue + ftype = (m.get("type") or "").strip() + if ftype == "keyword": + # Tag keyword becomes the File field (tagged=True, name=keyword name) + rec_fields[rname] = v2.File( + name=rname, + longname=m.get("longname") or None, + description=m.get("description") or None, + optional=_to_bool(m.get("optional"), False), + developmode=_to_bool(m.get("developmode"), False), + netcdf=_to_bool(m.get("netcdf"), False), + tagged=True, + mode=file_mode, # type: ignore[arg-type] + ) + else: + rec_fields[rname] = __map_field(m) + else: + rec_fields = _record_fields() + return v2.Record( name=_name, longname=longname, diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 66a6b2bf..5b0b4831 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -9,6 +9,7 @@ from pydantic import ( BaseModel, computed_field, + model_serializer, model_validator, ) from pydantic import ( @@ -26,6 +27,14 @@ class FieldBase(BaseModel): netcdf: bool = False tagged: bool = True + @model_serializer(mode="wrap") + def _serialize(self, handler: Any) -> dict[str, Any]: + data = handler(self) + # `type` has a frozen default so exclude_defaults=True drops it; restore it. + if "type" not in data and "type" in type(self).model_fields: + data = {"type": getattr(self, "type"), **data} + return data + @classmethod def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": type_name = d.get("type") @@ -346,6 +355,13 @@ class ComponentBase(BaseModel): schema_version: str | None = None dims: dict[str, Dim] | None = None + @model_serializer(mode="wrap") + def _serialize(self, handler: Any) -> dict[str, Any]: + data = handler(self) + if "type" not in data and "type" in type(self).model_fields: + data = {"type": getattr(self, "type"), **data} + return data + class Simulation(ComponentBase): type: Literal["simulation"] = "simulation" diff --git a/pyproject.toml b/pyproject.toml index cf7f492a..afc551d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ docs = [ dfn = [ "boltons", "pooch", + "pyaml", "pydantic", "tomli", "tomli-w" @@ -82,6 +83,7 @@ models = [ "boltons", "filelock", "pooch", + "pyaml", "pydantic", "tomli", "tomli-w" @@ -125,6 +127,7 @@ docs = [ dfn = [ "boltons", "pooch", + "pyaml", "pydantic", "tomli", "tomli-w" @@ -134,6 +137,7 @@ models = [ "boltons", "filelock", "pooch", + "pyaml", "pydantic", "tomli", "tomli-w" From 1de7a2095fa969402c9c365333ebc4544eda123b Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 13:31:38 -0700 Subject: [PATCH 26/29] fixes --- modflow_devtools/dfn/schema.py | 10 ++--- modflow_devtools/dfnmap.py | 27 ++++++++++- modflow_devtools/dfns/mapper.py | 45 ++++++++++++++----- modflow_devtools/dfns/schema.py | 80 +++++++++++++++++++++++++++++---- 4 files changed, 135 insertions(+), 27 deletions(-) diff --git a/modflow_devtools/dfn/schema.py b/modflow_devtools/dfn/schema.py index 5ce62af2..3fc365a1 100644 --- a/modflow_devtools/dfn/schema.py +++ b/modflow_devtools/dfn/schema.py @@ -627,6 +627,9 @@ def _load_v2(cls, f, name, fmt: str = "toml") -> "Dfn": raise ValueError(f"Unsupported format: {fmt!r}") if name and name != data.get("name", None): raise ValueError(f"Name mismatch, expected {name}") + for block in (data.get("blocks") or {}).values(): + for field_name, field in block.items(): + field.setdefault("name", field_name) return cls(**data) @classmethod # type: ignore[misc] @@ -651,7 +654,7 @@ def load( return cls._load_v2(f, name, fmt="json") else: raise ValueError( - f"Unsupported version {version!r}, expected one of: 'dfn', 'toml', 'yaml', 'json', 1, 2" + f"Unsupported version {version!r}, expected one of: 'dfn', 'toml', 'yaml', 'json'" ) @staticmethod # type: ignore[misc] @@ -668,10 +671,7 @@ def load_all(dfndir: PathLike, version: FormatVersion | None = None) -> Dfns: dfn_paths: list[Path] = [p for p in dfndir.glob("*.dfn") if p.stem not in _EXCLUDE] toml_paths: list[Path] = [p for p in dfndir.glob("*.toml") if p.stem not in _EXCLUDE] yaml_paths: list[Path] = [ - p - for ext in ("*.yaml", "*.yml") - for p in dfndir.glob(ext) - if p.stem not in _EXCLUDE + p for ext in ("*.yaml", "*.yml") for p in dfndir.glob(ext) if p.stem not in _EXCLUDE ] json_paths: list[Path] = [p for p in dfndir.glob("*.json") if p.stem not in _EXCLUDE] diff --git a/modflow_devtools/dfnmap.py b/modflow_devtools/dfnmap.py index f039a753..61398916 100644 --- a/modflow_devtools/dfnmap.py +++ b/modflow_devtools/dfnmap.py @@ -18,6 +18,12 @@ _EXT: dict[str, str] = {"yaml": ".yaml", "toml": ".toml", "json": ".json"} +# YAML 1.1 (PyYAML default) serializes booleans as yes/no; override to true/false (YAML 1.2). +pyaml.add_representer( + bool, + lambda dumper, v: dumper.represent_scalar("tag:yaml.org,2002:bool", "true" if v else "false"), +) + def _serialize_safe(obj: Any) -> Any: """Recursively coerce non-native types to primitives suitable for serialization.""" @@ -29,7 +35,12 @@ def _serialize_safe(obj: Any) -> Any: exclude_defaults=True, ) if isinstance(obj, dict): - return {k: _serialize_safe(v) for k, v in obj.items() if v is not None} + result = {k: _serialize_safe(v) for k, v in obj.items() if v is not None} + # Strip redundant name from v1/v1.1 field dicts — name is the dict key in the parent block. + # (v2 Pydantic models handle this via their own serializers.) + if "name" in result and "type" in result: + del result["name"] + return result if isinstance(obj, list): return [_serialize_safe(v) for v in obj] if isinstance(obj, (str, int, float, bool)) or obj is None: @@ -37,7 +48,19 @@ def _serialize_safe(obj: Any) -> Any: return str(obj) # Version → str, etc. +def _scalars_first(obj: Any) -> Any: + """Recursively reorder dict keys so scalar values precede dicts and lists.""" + if isinstance(obj, dict): + scalars = {k: _scalars_first(v) for k, v in obj.items() if not isinstance(v, (dict, list))} + complex_ = {k: _scalars_first(v) for k, v in obj.items() if isinstance(v, (dict, list))} + return {**scalars, **complex_} + if isinstance(obj, list): + return [_scalars_first(v) for v in obj] + return obj + + def _write(data: dict, path: Path, fmt: Format) -> None: + data = _scalars_first(data) if fmt == "toml": with path.open("wb") as f: tomli_w.dump(data, f) @@ -46,7 +69,7 @@ def _write(data: dict, path: Path, fmt: Format) -> None: json.dump(data, f, indent=2) elif fmt == "yaml": with path.open("w") as f: - pyaml.dump(data, f) + pyaml.dump(data, f, vspacing=False, sort_keys=False) # mypy: ignore-errors diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index bee652e3..b9fb9df2 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -282,12 +282,18 @@ def _to_scalar() -> v2.Scalar: developmode=developmode, netcdf=netcdf, tagged=tagged, - valid=valid.split() if isinstance(valid, str) and valid else (list(valid) if valid else None), + valid=valid.split() + if isinstance(valid, str) and valid + else (list(valid) if valid else None), case_sensitive=preserve_case, time_series=time_series, ) if _type == "integer": - v = [int(x) for x in valid.split()] if isinstance(valid, str) and valid else ([int(x) for x in valid] if valid else None) + v = ( + [int(x) for x in valid.split()] + if isinstance(valid, str) and valid + else ([int(x) for x in valid] if valid else None) + ) return v2.Integer( name=_name, longname=longname, @@ -425,8 +431,12 @@ def _record_fields() -> dict: for sname in subnames: if sname in ("filein", "fileout"): m = next( - (fi for fi in fields.values(multi=True) - if fi["name"] == sname and try_parse_bool(fi.get("in_record", False))), + ( + fi + for fi in fields.values(multi=True) + if fi["name"] == sname + and try_parse_bool(fi.get("in_record", False)) + ), None, ) if m and (m.get("type") or "").strip() == "keyword": @@ -443,11 +453,19 @@ def _record_fields() -> dict: if sname == file_mode: continue m_s = next( - (fi for fi in fields.values(multi=True) - if fi["name"] == sname and try_parse_bool(fi.get("in_record", False))), + ( + fi + for fi in fields.values(multi=True) + if fi["name"] == sname + and try_parse_bool(fi.get("in_record", False)) + ), None, ) - if m_s and (m_s.get("type") or "").strip() == "string" and not _to_bool(m_s.get("tagged"), True): + if ( + m_s + and (m_s.get("type") or "").strip() == "string" + and not _to_bool(m_s.get("tagged"), True) + ): path_field_name = sname break @@ -456,10 +474,13 @@ def _record_fields() -> dict: if rname in (file_mode, path_field_name): continue # drop mode keyword and path string m = next( - (fi for fi in fields.values(multi=True) - if fi["name"] == rname - and try_parse_bool(fi.get("in_record", False)) - and not (fi.get("type") or "").startswith("record")), + ( + fi + for fi in fields.values(multi=True) + if fi["name"] == rname + and try_parse_bool(fi.get("in_record", False)) + and not (fi.get("type") or "").startswith("record") + ), None, ) if m is None: @@ -478,7 +499,7 @@ def _record_fields() -> dict: mode=file_mode, # type: ignore[arg-type] ) else: - rec_fields[rname] = __map_field(m) + rec_fields[rname] = __map_field(m) # type: ignore else: rec_fields = _record_fields() diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 5b0b4831..67a207c6 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -30,6 +30,7 @@ class FieldBase(BaseModel): @model_serializer(mode="wrap") def _serialize(self, handler: Any) -> dict[str, Any]: data = handler(self) + data.pop("name", None) # name is the dict key in the parent container # `type` has a frozen default so exclude_defaults=True drops it; restore it. if "type" not in data and "type" in type(self).model_fields: data = {"type": getattr(self, "type"), **data} @@ -129,6 +130,18 @@ class List(FieldBase): item: "Record | Union" shape: list[str] = [] + @model_serializer(mode="wrap") + def _serialize(self, handler: Any) -> dict[str, Any]: + data = handler(self) + data.pop("name", None) # name is the dict key in Block.fields + if "type" not in data: + data = {"type": "list", **data} + # item.name is stripped by FieldBase._serialize since item is a FieldBase subclass, + # but item is not stored as a dict key — re-inject its name. + if "item" in data and isinstance(data["item"], dict): + data["item"] = {"name": self.item.name, **data["item"]} + return data + @model_validator(mode="after") def _check_shape_length(self) -> "List": if len(self.shape) > 1: @@ -340,6 +353,12 @@ class Block(BaseModel): fields: dict[str, Field] repeats: bool = False + @model_serializer(mode="wrap") + def _serialize(self, handler: Any) -> dict[str, Any]: + data = handler(self) + data.pop("name", None) # name is the dict key in ComponentBase.blocks + return data + @property def optional(self) -> bool: return all(f.optional for f in self.fields.values()) @@ -349,16 +368,16 @@ def optional(self) -> bool: class ComponentBase(BaseModel): + schema_version: str | None = None name: str - blocks: dict[str, Block] | None = None parent: str | list[str] | None = None - schema_version: str | None = None dims: dict[str, Dim] | None = None + blocks: dict[str, Block] | None = None @model_serializer(mode="wrap") def _serialize(self, handler: Any) -> dict[str, Any]: data = handler(self) - if "type" not in data and "type" in type(self).model_fields: + if "type" not in data: data = {"type": getattr(self, "type"), **data} return data @@ -710,6 +729,26 @@ def _check_array(arr: "Array", enclosing: "Record | None") -> None: _check_array(subfield, item) +def _inject_field_names(fields: dict) -> None: + """Recursively inject name from dict key into field dicts.""" + for field_name, field in fields.items(): + field.setdefault("name", field_name) + _inject_field_names(field.get("fields") or {}) # Record.fields + _inject_field_names(field.get("arms") or {}) # Union.arms + item = field.get("item") + if isinstance(item, dict): + # List.item.name is re-injected during serialization; recurse into its children. + _inject_field_names(item.get("fields") or {}) + _inject_field_names(item.get("arms") or {}) + + +def _inject_names(comp_data: dict) -> None: + """Inject block and field names from dict keys before Pydantic validation.""" + for block_name, block in (comp_data.get("blocks") or {}).items(): + block.setdefault("name", block_name) + _inject_field_names(block.get("fields") or {}) + + class Dfns(BaseModel): """A set of component definitions.""" @@ -803,22 +842,47 @@ def _validate_relations(self) -> "Dfns": @classmethod def load(cls, path: str | PathLike) -> "Dfns": """Load a directory of definition files.""" + import json + + import yaml + from modflow_devtools.dfn import schema as v1 from modflow_devtools.dfns.mapper import map as map_v2 dfns: dict = {} path = Path(path).expanduser().resolve() - - dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.name not in v1.EXCLUDE_DFNS} - toml_paths = {p.stem: p for p in path.glob("*.toml") if p.name not in v1.EXCLUDE_DFNS} + _EXCLUDE = {"common", "flopy"} + + dfn_paths = {p.stem: p for p in path.glob("*.dfn") if p.stem not in _EXCLUDE} + toml_paths = {p.stem: p for p in path.glob("*.toml") if p.stem not in _EXCLUDE} + yaml_paths = { + p.stem: p + for ext in ("*.yaml", "*.yml") + for p in path.glob(ext) + if p.stem not in _EXCLUDE + } + json_paths = {p.stem: p for p in path.glob("*.json") if p.stem not in _EXCLUDE} if dfn_paths: dfns = v1.resolve_parents(v1.load_all(path)) dfns = {n: map_v2(d) for n, d in dfns.items()} - if toml_paths: + elif toml_paths: for toml_path in toml_paths.values(): with toml_path.open("rb") as f: dfn = tomli.load(f) - dfns[dfn["name"]] = dfn + _inject_names(dfn) + dfns[dfn["name"]] = dfn + elif yaml_paths: + for yaml_path in yaml_paths.values(): + with yaml_path.open() as f: + dfn = yaml.safe_load(f) + _inject_names(dfn) + dfns[dfn["name"]] = dfn + elif json_paths: + for json_path in json_paths.values(): + with json_path.open() as f: + dfn = json.load(f) + _inject_names(dfn) + dfns[dfn["name"]] = dfn return cls(components=dfns) From 84b7a8bb0cfe3befb1a040188991fcea1ad4e091 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 14:12:34 -0700 Subject: [PATCH 27/29] more fixes --- modflow_devtools/dfnmap.py | 4 +++- modflow_devtools/dfns/schema.py | 39 +++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/modflow_devtools/dfnmap.py b/modflow_devtools/dfnmap.py index 61398916..f747131d 100644 --- a/modflow_devtools/dfnmap.py +++ b/modflow_devtools/dfnmap.py @@ -29,7 +29,10 @@ def _serialize_safe(obj: Any) -> Any: """Recursively coerce non-native types to primitives suitable for serialization.""" if isinstance(obj, BaseModel): + # strip_names context propagates through v2 FieldBase/_Block serializers; + # ignored harmlessly by v1/v1.1 models that don't inspect it. return obj.model_dump( + context={"strip_names": True}, exclude_none=True, exclude_unset=True, exclude_defaults=True, @@ -37,7 +40,6 @@ def _serialize_safe(obj: Any) -> Any: if isinstance(obj, dict): result = {k: _serialize_safe(v) for k, v in obj.items() if v is not None} # Strip redundant name from v1/v1.1 field dicts — name is the dict key in the parent block. - # (v2 Pydantic models handle this via their own serializers.) if "name" in result and "type" in result: del result["name"] return result diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 67a207c6..56627227 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -8,6 +8,7 @@ import tomli from pydantic import ( BaseModel, + SerializationInfo, computed_field, model_serializer, model_validator, @@ -28,16 +29,29 @@ class FieldBase(BaseModel): tagged: bool = True @model_serializer(mode="wrap") - def _serialize(self, handler: Any) -> dict[str, Any]: + def _serialize(self, handler: Any, info: SerializationInfo) -> dict[str, Any]: data = handler(self) - data.pop("name", None) # name is the dict key in the parent container + if info.context and info.context.get("strip_names"): + data.pop("name", None) # `type` has a frozen default so exclude_defaults=True drops it; restore it. if "type" not in data and "type" in type(self).model_fields: data = {"type": getattr(self, "type"), **data} return data + def dump(self, *, strip_names: bool = True, **kwargs) -> dict[str, Any]: + if strip_names: + kwargs["context"] = {**(kwargs.get("context") or {}), "strip_names": True} + return self.model_dump(**kwargs) + + def dump_json(self, *, strip_names: bool = True, **kwargs) -> str: + if strip_names: + kwargs["context"] = {**(kwargs.get("context") or {}), "strip_names": True} + return self.model_dump_json(**kwargs) + @classmethod - def from_dict(cls, d: dict, strict: bool = False) -> "FieldBase": + def from_dict(cls, d: dict, name: str | None = None, strict: bool = False) -> "FieldBase": + if name is not None: + d = {"name": name, **d} type_name = d.get("type") type_map: dict[str | None, type[FieldBase]] = { "keyword": Keyword, @@ -131,13 +145,14 @@ class List(FieldBase): shape: list[str] = [] @model_serializer(mode="wrap") - def _serialize(self, handler: Any) -> dict[str, Any]: + def _serialize(self, handler: Any, info: SerializationInfo) -> dict[str, Any]: data = handler(self) - data.pop("name", None) # name is the dict key in Block.fields + if info.context and info.context.get("strip_names"): + data.pop("name", None) if "type" not in data: data = {"type": "list", **data} - # item.name is stripped by FieldBase._serialize since item is a FieldBase subclass, - # but item is not stored as a dict key — re-inject its name. + # item is stored as an attribute, not a dict key, so its name is never + # implicit — always re-inject it regardless of strip_names. if "item" in data and isinstance(data["item"], dict): data["item"] = {"name": self.item.name, **data["item"]} return data @@ -359,6 +374,16 @@ def _serialize(self, handler: Any) -> dict[str, Any]: data.pop("name", None) # name is the dict key in ComponentBase.blocks return data + def dump(self, *, strip_names: bool = True, **kwargs) -> dict[str, Any]: + if strip_names: + kwargs["context"] = {**(kwargs.get("context") or {}), "strip_names": True} + return self.model_dump(**kwargs) + + def dump_json(self, *, strip_names: bool = True, **kwargs) -> str: + if strip_names: + kwargs["context"] = {**(kwargs.get("context") or {}), "strip_names": True} + return self.model_dump_json(**kwargs) + @property def optional(self) -> bool: return all(f.optional for f in self.fields.values()) From 916f605764b5f142d126ba7da476548db273879f Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 18:35:13 -0700 Subject: [PATCH 28/29] shape fixes, etc --- autotest/dfns/test_mapper.py | 102 +++++++++++++++++++++++++++- autotest/test_dfnmap.py | 19 ------ modflow_devtools/dfnmap.py | 24 ++----- modflow_devtools/dfns/mapper.py | 115 ++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 37 deletions(-) diff --git a/autotest/dfns/test_mapper.py b/autotest/dfns/test_mapper.py index b44af973..eb7d83f1 100644 --- a/autotest/dfns/test_mapper.py +++ b/autotest/dfns/test_mapper.py @@ -179,6 +179,14 @@ def test_map_recarray_conversion(): dfn = _v1_dfn( name="test-pkg", blocks={ + "dimensions": { + "maxbound": _v1_field( + name="maxbound", + type="integer", + block="dimensions", + in_record=False, + ), + }, "period": { "stress_period_data": _v1_field( name="stress_period_data", @@ -199,13 +207,105 @@ def test_map_recarray_conversion(): block="period", in_record=True, ), - } + }, }, ) component = map_v2(dfn) period_fields = component.blocks["period"].fields spd = period_fields["stress_period_data"] assert isinstance(spd, List) + assert spd.shape == ["maxbound"] assert isinstance(spd.item, Record) assert "cellid" in spd.item.fields assert "q" in spd.item.fields + + +def test_map_recarray_missing_shape_inferred_from_maxbound(): + """Period list with empty shape gets shape=["maxbound"] when maxbound dim exists.""" + dfn = _v1_dfn( + name="utl-spc", + blocks={ + "dimensions": { + "maxbound": _v1_field( + name="maxbound", + type="integer", + block="dimensions", + in_record=False, + ), + }, + "period": { + "spd": _v1_field( + name="spd", + type="recarray bndno spcsetting", + block="period", + shape="", # empty in v1 + ), + "bndno": _v1_field( + name="bndno", + type="integer", + block="period", + in_record=True, + ), + "spcsetting": _v1_field( + name="spcsetting", + type="keystring concentration", + block="period", + in_record=True, + ), + "concentration": _v1_field( + name="concentration", + type="double precision", + block="period", + tagged=True, + in_record=True, + ), + }, + }, + ) + component = map_v2(dfn) + period_fields = component.blocks["period"].fields + spd = period_fields["spd"] + assert isinstance(spd, List) + assert spd.shape == ["maxbound"] + + +def test_map_recarray_no_shape_no_maxbound(): + """Period list with no shape and no maxbound dim keeps shape=[].""" + dfn = _v1_dfn( + name="gwf-sfr", + advanced=True, + blocks={ + "period": { + "perioddata": _v1_field( + name="perioddata", + type="recarray ifno sfrsetting", + block="period", + shape="", + ), + "ifno": _v1_field( + name="ifno", + type="integer", + block="period", + in_record=True, + ), + "sfrsetting": _v1_field( + name="sfrsetting", + type="keystring status", + block="period", + in_record=True, + ), + "status": _v1_field( + name="status", + type="string", + block="period", + tagged=True, + in_record=True, + ), + }, + }, + ) + component = map_v2(dfn) + period_fields = component.blocks["period"].fields + lst = period_fields["perioddata"] + assert isinstance(lst, List) + assert lst.shape == [] diff --git a/autotest/test_dfnmap.py b/autotest/test_dfnmap.py index 582138d0..840a87c4 100644 --- a/autotest/test_dfnmap.py +++ b/autotest/test_dfnmap.py @@ -36,14 +36,6 @@ def dfn_dir(module_tmpdir): return path -@pytest.fixture(scope="module", params=FORMATS) -def converted_v1(request, dfn_dir, module_tmpdir): - fmt = request.param - out = module_tmpdir / f"v1-{fmt}" - migrate(dfn_dir, out, schema_version="1", fmt=fmt) - return out, fmt - - @pytest.fixture(scope="module", params=FORMATS) def converted_v1_1(request, dfn_dir, module_tmpdir): fmt = request.param @@ -60,17 +52,6 @@ def converted_v2(request, dfn_dir, module_tmpdir): return out, fmt -@requires_pkg("boltons") -def test_convert_v1(converted_v1): - out, fmt = converted_v1 - files = list(out.glob(f"*.{fmt}")) - assert files - for p in files: - data = _load(p, fmt) - assert data["name"] == p.stem - assert data["schema_version"] == "1" - - @requires_pkg("boltons") def test_convert_v1_1(converted_v1_1): out, fmt = converted_v1_1 diff --git a/modflow_devtools/dfnmap.py b/modflow_devtools/dfnmap.py index f747131d..804feb53 100644 --- a/modflow_devtools/dfnmap.py +++ b/modflow_devtools/dfnmap.py @@ -16,8 +16,6 @@ Format = Literal["yaml", "toml", "json"] -_EXT: dict[str, str] = {"yaml": ".yaml", "toml": ".toml", "json": ".json"} - # YAML 1.1 (PyYAML default) serializes booleans as yes/no; override to true/false (YAML 1.2). pyaml.add_representer( bool, @@ -92,14 +90,14 @@ def migrate( outdir : str or PathLike Output directory. schema_version : str, optional - Target schema version: "1", "1.1", or "2". Default "2". + Target schema version: "1.1" or "2". Default "2". fmt : str, optional Output format: "yaml", "toml", or "json". Default "yaml". """ inpath = Path(inpath).expanduser().absolute() outdir = Path(outdir).expanduser().absolute() outdir.mkdir(exist_ok=True, parents=True) - ext = _EXT[fmt] + ext = f".{fmt}" if inpath.is_file(): if inpath.name == "common.dfn": @@ -113,32 +111,24 @@ def migrate( with inpath.open() as f: dfn = v1.Dfn.load(f, name=inpath.stem, common=common) - if schema_version == "1": - pass - elif schema_version == "1.1": + if schema_version == "1.1": dfn = map_v1_1(dfn) elif schema_version == "2": dfn = map_v2(dfn) else: - raise ValueError( - f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" - ) + raise ValueError(f"Got schema version {schema_version}, supported versions are: 1.1, 2") _write(_serialize_safe(dfn), outdir / f"{inpath.stem}{ext}", fmt) else: dfns = v1.load_all(inpath) - if schema_version == "1": - pass - elif schema_version == "1.1": + if schema_version == "1.1": dfns = v1.to_flat(v1.to_tree(dfns)) dfns = {name: map_v1_1(dfn) for name, dfn in dfns.items()} elif schema_version == "2": dfns = {name: map_v2(dfn) for name, dfn in dfns.items()} else: - raise ValueError( - f"Got schema version {schema_version}, supported versions are: 1, 1.1, 2" - ) + raise ValueError(f"Got schema version {schema_version}, supported versions are: 1.1, 2") for dfn_name, dfn in dfns.items(): _write(_serialize_safe(dfn), outdir / f"{dfn_name}{ext}", fmt) @@ -164,7 +154,7 @@ def migrate( "--schema-version", "-s", default="2", - choices=["1", "1.1", "2"], + choices=["1.1", "2"], help="Target schema version (default: 2).", ) parser.add_argument( diff --git a/modflow_devtools/dfns/mapper.py b/modflow_devtools/dfns/mapper.py index b9fb9df2..71f2b410 100644 --- a/modflow_devtools/dfns/mapper.py +++ b/modflow_devtools/dfns/mapper.py @@ -31,6 +31,59 @@ def _scope_for( return "component" +def _raw_dim_names(blocks: dict[str, v2.Block]) -> set[str]: + """Names of all Integer fields in the dimensions block.""" + dim_block = blocks.get("dimensions") + if not dim_block: + return set() + return {fname for fname, f in dim_block.fields.items() if isinstance(f, v2.Integer)} + + +def _parse_list_shape(s: str) -> list[str]: + """ + Parse a v1 recarray shape string into a ``List.shape`` value. + + Only a bare identifier is accepted — complex expressions such as + ``sum(nlakeconn)`` cannot be represented in ``List.shape`` and are dropped. + """ + if not s: + return [] + s_clean = s.strip() + if s_clean.startswith("(") and s_clean.endswith(")"): + s_clean = s_clean[1:-1].strip() + if _IDENT_RE.fullmatch(s_clean): + return [s_clean] + return [] + + +def _normalize_n_prefix_shapes( + blocks: dict[str, v2.Block], + raw_dim_names: set[str], +) -> dict[str, v2.Block]: + """ + Fix List shapes that use ``nFoo`` where the actual dimension is ``maxFoo``. + + Some v1 DFNs (e.g. ``gwf-mvr [packages]`` with ``shape (npackages)``) use + an ``n``-prefixed name while the dimensions block defines the same quantity + under a ``max``-prefixed name. Normalise before building explicit dims. + """ + result = {} + for bname, block in blocks.items(): + new_fields = {} + changed = False + for fname, field in block.fields.items(): + if isinstance(field, v2.List) and field.shape: + elem = field.shape[0] + if elem not in raw_dim_names and elem.startswith("n") and len(elem) > 1: + candidate = "max" + elem[1:] + if candidate in raw_dim_names: + field = field.model_copy(update={"shape": [candidate]}) + changed = True + new_fields[fname] = field + result[bname] = block.model_copy(update={"fields": new_fields}) if changed else block + return result + + def _build_explicit_dims( parent: "str | list[str] | None", blocks: dict[str, v2.Block], @@ -65,6 +118,33 @@ def _build_explicit_dims( return dims +def _sanitize_list_shapes( + blocks: dict[str, v2.Block], + known_dims: set[str], +) -> dict[str, v2.Block]: + """ + Clear the shape of any List whose shape element doesn't resolve to a known + dim. + + Advanced packages (LAK, SFR, GNC, transport packages, etc.) often carry + ``shape (maxbound)`` in their v1 DFNs as a convention even though + ``maxbound`` is not declared as a dimension. The structurally correct v2 + representation for such lists is ``shape=[]``. + """ + result = {} + for bname, block in blocks.items(): + new_fields = {} + changed = False + for fname, field in block.fields.items(): + if isinstance(field, v2.List) and field.shape: + if any(elem not in known_dims for elem in field.shape): + field = field.model_copy(update={"shape": []}) + changed = True + new_fields[fname] = field + result[bname] = block.model_copy(update={"fields": new_fields}) if changed else block + return result + + def _resolve_dimensions( blocks: dict[str, v2.Block], ) -> tuple[dict[str, v2.Block], dict[str, v2.Dim]]: @@ -176,6 +256,34 @@ def _resolve_record(record: v2.Record) -> v2.Record: } +def _fill_period_list_shapes( + blocks: dict[str, v2.Block], + explicit_dims: dict[str, v2.Dim], +) -> dict[str, v2.Block]: + """ + For period blocks whose List field has no shape expression, infer the shape + from the component's explicit dims. Currently handles ``maxbound`` only: + if the component defines a ``maxbound`` dimension but the period list omits + it, add ``shape=["maxbound"]``. + """ + if "maxbound" not in explicit_dims: + return blocks + result = {} + for bname, block in blocks.items(): + if "period" not in bname: + result[bname] = block + continue + new_fields = {} + changed = False + for fname, field in block.fields.items(): + if isinstance(field, v2.List) and not field.shape: + field = field.model_copy(update={"shape": ["maxbound"]}) + changed = True + new_fields[fname] = field + result[bname] = block.model_copy(update={"fields": new_fields}) if changed else block + return result + + def map(dfn: v1.Dfn) -> v2.Component: """Map a component definition from the v1 schema to v2.""" @@ -401,6 +509,7 @@ def _record_fields() -> dict: if _type.startswith("recarray"): item = _row_field() + list_shape = _parse_list_shape(shape_str) if shape_str else [] return v2.List( name=_name, longname=longname, @@ -410,6 +519,7 @@ def _record_fields() -> dict: developmode=developmode, netcdf=netcdf, item=item, + shape=list_shape, ) if _type.startswith("keystring"): @@ -572,7 +682,12 @@ def _record_fields() -> dict: blocks, array_dims = _resolve_dimensions(blocks) blocks = _resolve_relations(blocks) + raw_dim_names = _raw_dim_names(blocks) + blocks = _normalize_n_prefix_shapes(blocks, raw_dim_names) explicit_dims = _build_explicit_dims(dfn["parent"], blocks) + known_dims = set(explicit_dims) | set(array_dims) + blocks = _sanitize_list_shapes(blocks, known_dims) + blocks = _fill_period_list_shapes(blocks, explicit_dims) dims = {**explicit_dims, **array_dims} or None d: dict[str, Any] = { From 117bdf859220a6d65badec57449b6484ac335868 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 22 May 2026 18:37:21 -0700 Subject: [PATCH 29/29] describe traditional vs advanced stress package maxbound conventions --- docs/md/dfnspec.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index da9e8c6d..44e2a346 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -176,11 +176,13 @@ Optional discriminator indicating the package's functional role. Several package - `"solution"`: provides solving capability for models. Compatibility with a model is determined by the model's `solution` attribute. - `"exchange"`: connects two models, enabling them to share boundary conditions or state at their interface. Parent is the simulation. -- `"stress"`: imposes boundary conditions on a model. Period data is provided per stress period; each period block replaces the full set of stresses for that period. +- `"stress"`: imposes boundary conditions on a model. Period data is provided per stress period; each period block replaces the full set of stresses for that period. Stress packages always declare a `maxbound` integer dimension (the maximum number of boundary entries per stress period) and their period block list carries `shape: ["maxbound"]`. - `"advanced"`: an advanced stress package. Differs from `"stress"` in three key ways: 1. Solves a continuity equation. Each feature (well, reach, lake cell, UZF cell) internally balances inflows, outflows, and change in storage. Traditional stress packages impose static conditions and do not have an internal water budget. **Note:** advanced packages can act as receivers in the Water Mover (MVR/MVE) package because they have an internal continuity equation to receive diverted water into. Traditional stress packages cannot. 2. Has dynamic state variables. Advanced packages compute a dependent variable (e.g., lake stage, well head, reach stage) that is part of the solution. Traditional stress packages use fixed/user-specified values. 3. Stress periods have feature replacement rather than block replacement semantics: when a new period block configuration is provided, traditional stress packages replace the entire previous configuration; advanced packages perform partial updates, modifying only features explicitly appearing in the new period block. **Note:** both simple and advanced packages fill-forward across omitted stress periods; the distinction is only in what happens when a new period block configuration is specified. + + Advanced packages do **not** declare a `maxbound` dimension. Their list lengths (packagedata rows, period entries per feature, etc.) are determined at runtime — typically derived from a linked flow package's budget object or internal state — and are not declared in the DFN. Accordingly, their list fields carry `shape: []`. - `"utility"`: an auxiliary package that may be attached to models or packages, such as time series, time-array series, or observations. Utility packages (`utl-*`) are distinguished from primary model input packages by providing configurational or cross-cutting concerns rather than representing a first-class hydrologic process. They may support `multi`. `subtype: null` (the default) covers packages that don't fall into any named category, such as output control packages. @@ -448,7 +450,13 @@ Type `list`. Collection type. Unlimited but for one rule: a list may not contain ###### `shape` -`[string] (default: [])`. The list's shape, as a list of shape expressions. May have at most one element, as lists are necessarily 1-dimensional. +`[string] (default: [])`. The list's shape, as a list of at most one shape expression (lists are necessarily 1-dimensional). + +An **empty `shape`** means the list length is unconstrained at schema-definition time. This is the correct representation for any list whose length is determined at runtime rather than from a declared dimension — including all list fields in advanced packages (LAK, SFR, MAW, UZF, and their transport-side counterparts), whose lengths are derived from a linked flow package's budget or internal state and are never declared in the DFN. + +A **non-empty `shape`** (exactly one element) names a declared dimension that bounds the list. The canonical case is a stress package's period block list, which carries `shape: ["maxbound"]`. The `maxbound` dimension is explicitly declared in the stress package's `dimensions` block as an integer field the user must supply. + +**Note:** Some non-period lists in stress-type packages (e.g. `utl-spc`) also reference `maxbound`; these follow the same rule — `maxbound` must be an explicitly declared dimension for the shape to be meaningful. ### Dimensions