diff --git a/flopy4/mf6/converter.py b/flopy4/mf6/converter.py index f298032d..ee912ff3 100644 --- a/flopy4/mf6/converter.py +++ b/flopy4/mf6/converter.py @@ -18,26 +18,17 @@ from flopy4.mf6.config import SPARSE_THRESHOLD from flopy4.mf6.constants import FILL_DNODATA from flopy4.mf6.context import Context -from flopy4.mf6.spec import fields_dict +from flopy4.mf6.spec import FileInOut -def _attach_field_metadata( - dataset: xr.Dataset, component_type: type, field_names: list[str] -) -> None: - # TODO: attach metadata to array attrs instead of dataset attrs - field_metadata = {} - component_fields = fields_dict(component_type) - for field_name in field_names: - if field_name in component_fields: - field_metadata[field_name] = component_fields[field_name].metadata - dataset.attrs["field_metadata"] = field_metadata - - -def _path_to_tuple(field_name: str, path_value: Path) -> tuple: - if field_name.endswith("_file"): - base_name = field_name.replace("_file", "").upper() - return (base_name, "FILEOUT", str(path_value)) - return (field_name.upper(), "FILEOUT", str(path_value)) +def path_to_tuple(name: str, value: Path, inout: FileInOut) -> tuple[str, ...]: + t = [name.upper()] + if name.endswith("_file"): + t[0] = name.replace("_file", "").upper() + if inout: + t.append(inout.upper()) + t.append(str(value)) + return tuple(t) def get_binding_blocks(value: Component) -> dict[str, dict[str, list[tuple[str, ...]]]]: @@ -100,9 +91,11 @@ def unstructure_component(value: Component) -> dict[str, Any]: # (and split the period data into separate kper-indexed blocks) # - other values to their original form if isinstance(field_value, Path): - rec = _path_to_tuple(field_name, field_value) + field_spec = xatspec.attrs[field_name] + field_meta = getattr(field_spec, "metadata", {}) + t = path_to_tuple(field_name, field_value, inout=field_meta.get("inout", "fileout")) # name may have changed e.g dropping '_file' suffix - blocks[block_name][rec[0]] = rec + blocks[block_name][t[0]] = t elif isinstance(field_value, datetime): blocks[block_name][field_name] = field_value.isoformat() elif ( @@ -165,7 +158,6 @@ def unstructure_component(value: Component) -> dict[str, Any]: if block_name in period_data and isinstance(period_data[block_name], dict): dataset = xr.Dataset(period_data[block_name]) - _attach_field_metadata(dataset, type(value), list(period_data[block_name].keys())) # type: ignore blocks[block_name] = {block_name: dataset} del period_data[block_name] @@ -177,7 +169,6 @@ def unstructure_component(value: Component) -> dict[str, Any]: for kper, block in period_blocks.items(): dataset = xr.Dataset(block) - _attach_field_metadata(dataset, type(value), list(block.keys())) blocks[f"{block_name} {kper + 1}"] = {block_name: dataset} # make sure options block always comes first diff --git a/flopy4/mf6/gwf/__init__.py b/flopy4/mf6/gwf/__init__.py index 70a64443..d2051a7f 100644 --- a/flopy4/mf6/gwf/__init__.py +++ b/flopy4/mf6/gwf/__init__.py @@ -15,8 +15,9 @@ from flopy4.mf6.gwf.oc import Oc from flopy4.mf6.gwf.wel import Wel from flopy4.mf6.model import Model -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path from flopy4.mf6.utils import open_cbc, open_hds +from flopy4.utils import to_path __all__ = ["Gwf", "Chd", "Dis", "Drn", "Ic", "Npf", "Oc", "Wel"] @@ -61,9 +62,15 @@ def budget(self): print_flows: bool = field(block="options", default=False) save_flows: bool = field(block="options", default=False) newtonoptions: Optional[NewtonOptions] = field(block="options", default=None) - nc_mesh2d_filerecord: Optional[Path] = field(block="options", default=None) - nc_structured_filerecord: Optional[Path] = field(block="options", default=None) - nc_filerecord: Optional[Path] = field(block="options", default=None) + nc_mesh2d_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) + nc_structured_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) + nc_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) dis: Dis = field(converter=convert_grid, block="packages") ic: Ic = field(block="packages") oc: Oc = field(block="packages") diff --git a/flopy4/mf6/gwf/chd.py b/flopy4/mf6/gwf/chd.py index 52c9cee1..c0e82727 100644 --- a/flopy4/mf6/gwf/chd.py +++ b/flopy4/mf6/gwf/chd.py @@ -9,7 +9,8 @@ from flopy4.mf6.component import update_maxbound from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -21,8 +22,12 @@ class Chd(Package): print_input: bool = field(block="options", default=False) print_flows: bool = field(block="options", default=False) save_flows: bool = field(block="options", default=False) - ts_filerecord: Optional[Path] = field(block="options", default=None) - obs_filerecord: Optional[Path] = field(block="options", default=None) + ts_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) + obs_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) dev_no_newton: bool = field(default=False, block="options") maxbound: Optional[int] = field(block="dimensions", default=None, init=False) head: Optional[NDArray[np.float64]] = array( diff --git a/flopy4/mf6/gwf/drn.py b/flopy4/mf6/gwf/drn.py index 220346ef..d3fbd7aa 100644 --- a/flopy4/mf6/gwf/drn.py +++ b/flopy4/mf6/gwf/drn.py @@ -9,7 +9,8 @@ from flopy4.mf6.component import update_maxbound from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -22,8 +23,12 @@ class Drn(Package): print_input: bool = field(block="options", default=False) print_flows: bool = field(block="options", default=False) save_flows: bool = field(block="options", default=False) - ts_filerecord: Optional[Path] = field(block="options", default=None) - obs_filerecord: Optional[Path] = field(block="options", default=None) + ts_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) + obs_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) mover: bool = field(block="options", default=False) dev_cubic_scaling: bool = field(default=False, block="options") maxbound: Optional[int] = field(block="dimensions", default=None, init=False) diff --git a/flopy4/mf6/gwf/npf.py b/flopy4/mf6/gwf/npf.py index 370d666d..168fec26 100644 --- a/flopy4/mf6/gwf/npf.py +++ b/flopy4/mf6/gwf/npf.py @@ -8,7 +8,8 @@ from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -42,7 +43,9 @@ class Xt3dOptions: save_saturation: bool = field(block="options", default=None) k22overk: bool = field(block="options", default=None) k33overk: bool = field(block="options", default=None) - tvk_filerecord: Optional[Path] = field(block="options", default=None) + tvk_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) export_array_ascii: bool = field(block="options", default=False) export_array_netcdf: bool = field(block="options", default=False) dev_no_newton: bool = field(block="options", default=False) diff --git a/flopy4/mf6/gwf/oc.py b/flopy4/mf6/gwf/oc.py index 00a5e8d5..f9ac369c 100644 --- a/flopy4/mf6/gwf/oc.py +++ b/flopy4/mf6/gwf/oc.py @@ -8,7 +8,7 @@ from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path from flopy4.utils import to_path @@ -35,20 +35,14 @@ class Period: rtype: str = field() steps: "Oc.Steps" = field() - budget_file: Optional[Path] = field( - block="options", - converter=to_path, - default=None, + budget_file: Optional[Path] = path( + block="options", converter=to_path, default=None, inout="fileout" ) - budget_csv_file: Optional[Path] = field( - block="options", - converter=to_path, - default=None, + budget_csv_file: Optional[Path] = path( + block="options", converter=to_path, default=None, inout="fileout" ) - head_file: Optional[Path] = field( - block="options", - converter=to_path, - default=None, + head_file: Optional[Path] = path( + block="options", converter=to_path, default=None, inout="fileout" ) # TODO: needs coverter and then rename? head: Optional[Format] = field(block="options", default=None) diff --git a/flopy4/mf6/gwf/rch.py b/flopy4/mf6/gwf/rch.py index cf36d887..7e444a3e 100644 --- a/flopy4/mf6/gwf/rch.py +++ b/flopy4/mf6/gwf/rch.py @@ -9,7 +9,8 @@ from flopy4.mf6.component import update_maxbound from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -22,8 +23,12 @@ class Rch(Package): print_input: bool = field(block="options", default=False) print_flows: bool = field(block="options", default=False) save_flows: bool = field(block="options", default=False) - ts_filerecord: Optional[Path] = field(block="options", default=None) - obs_filerecord: Optional[Path] = field(block="options", default=None) + ts_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) + obs_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) maxbound: Optional[int] = field(block="dimensions", default=None, init=False) recharge: Optional[NDArray[np.float64]] = array( block="period", diff --git a/flopy4/mf6/gwf/sto.py b/flopy4/mf6/gwf/sto.py index ac1e20c7..b08bc1ae 100644 --- a/flopy4/mf6/gwf/sto.py +++ b/flopy4/mf6/gwf/sto.py @@ -8,7 +8,8 @@ from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -16,7 +17,9 @@ class Sto(Package): save_flows: bool = field(block="options", default=False) storagecoefficient: bool = field(block="options", default=False) ss_confined_only: bool = field(block="options", default=False) - tvs_filerecord: Optional[Path] = field(block="options", default=None) + tvs_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) export_array_ascii: bool = field(block="options", default=False) export_array_netcdf: bool = field(block="options", default=False) dev_original_specific_storage: bool = field(block="options", default=False) diff --git a/flopy4/mf6/gwf/wel.py b/flopy4/mf6/gwf/wel.py index b8497f14..f86b59cf 100644 --- a/flopy4/mf6/gwf/wel.py +++ b/flopy4/mf6/gwf/wel.py @@ -9,7 +9,8 @@ from flopy4.mf6.component import update_maxbound from flopy4.mf6.converter import dict_to_array from flopy4.mf6.package import Package -from flopy4.mf6.spec import array, field +from flopy4.mf6.spec import array, field, path +from flopy4.utils import to_path @xattree @@ -22,9 +23,15 @@ class Wel(Package): print_flows: bool = field(block="options", default=False) save_flows: bool = field(block="options", default=False) auto_flow_reduce: float = field(block="options", default=None) - afrcsv_filerecord: Optional[Path] = field(block="options", default=None) - ts_filerecord: Optional[Path] = field(block="options", default=None) - obs_filerecord: Optional[Path] = field(block="options", default=None) + afrcsv_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) + ts_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="filein" + ) + obs_filerecord: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) mover: bool = field(block="options", default=False) maxbound: Optional[int] = field(block="dimensions", default=None, init=False) q: Optional[NDArray[np.float64]] = array( diff --git a/flopy4/mf6/ims.py b/flopy4/mf6/ims.py index 178ea128..99dd335d 100644 --- a/flopy4/mf6/ims.py +++ b/flopy4/mf6/ims.py @@ -4,7 +4,8 @@ from xattree import xattree from flopy4.mf6.solution import Solution -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path +from flopy4.utils import to_path @xattree @@ -13,14 +14,18 @@ class Ims(Solution): print_option: Optional[str] = field(block="options", default=None) complexity: str = field(block="options", default="simple") - csv_outer_output_file: Optional[Path] = field(default=None, block="options") - csv_inner_output_file: Optional[Path] = field(block="options", default=None) - no_ptc: bool = field(default=False, block="options") - no_ptc_option: Optional[str] = field(default=None, block="options") + csv_outer_output_file: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) + csv_inner_output_file: Optional[Path] = path( + block="options", default=None, converter=to_path, inout="fileout" + ) + no_ptc: bool = field(block="options", default=False) + no_ptc_option: Optional[str] = field(block="options", default=None) ats_outer_maximum_fraction: Optional[float] = field(block="options", default=None) - outer_dvclose: Optional[float] = field(default=None, block="nonlinear") - outer_maximum: Optional[int] = field(default=None, block="nonlinear") - under_relaxation: Optional[str] = field(default=None, block="nonlinear") + outer_dvclose: Optional[float] = field(block="nonlinear", default=None) + outer_maximum: Optional[int] = field(block="nonlinear", default=None) + under_relaxation: Optional[str] = field(block="nonlinear", default=None) under_relaxation_gamma: Optional[float] = field(block="nonlinear", default=None) under_relaxation_theta: Optional[float] = field(block="nonlinear", default=None) under_relaxation_kappa: Optional[float] = field(block="nonlinear", default=None) diff --git a/flopy4/mf6/spec.py b/flopy4/mf6/spec.py index b6131ede..d6e0dc7a 100644 --- a/flopy4/mf6/spec.py +++ b/flopy4/mf6/spec.py @@ -7,7 +7,7 @@ import types from datetime import datetime from pathlib import Path -from typing import Union, get_args, get_origin +from typing import Literal, Union, get_args, get_origin import numpy as np from attrs import NOTHING, Attribute @@ -49,6 +49,40 @@ def field( ) +FileInOut = Literal[None, "filein", "fileout"] + + +def path( + default=NOTHING, + validator=None, + converter=None, + repr=True, + eq=True, + init=True, + metadata=None, + on_setattr=None, + block: str | None = None, + inout: FileInOut | None = None, +): + """Define a path field.""" + if block: + metadata = metadata or {} + metadata["block"] = block + if inout: + metadata = metadata or {} + metadata["inout"] = inout + return flopy_field( + default=default, + validator=validator, + converter=converter, + repr=repr, + eq=eq, + init=init, + on_setattr=on_setattr, + metadata=metadata, + ) + + def dim( scope=None, coord: bool | str = True,