From fa3d752e05a82b22d065e3c9d9ce04af42853067 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 4 Sep 2025 09:57:24 +1200 Subject: [PATCH 1/2] refactor(docs): mf6 classes and codegen templates --- flopy/mf6/mfmodel.py | 77 +- flopy/mf6/mfpackage.py | 16 +- flopy/mf6/mfsimbase.py | 123 +-- flopy/mf6/utils/codegen/__init__.py | 49 +- flopy/mf6/utils/codegen/filters.py | 966 +++++++++--------- .../utils/codegen/templates/exchange.py.jinja | 60 +- .../utils/codegen/templates/model.py.jinja | 71 +- .../utils/codegen/templates/package.py.jinja | 73 +- .../codegen/templates/simulation.py.jinja | 129 ++- flopy/mf6/utils/generate_classes.py | 7 +- 10 files changed, 809 insertions(+), 762 deletions(-) diff --git a/flopy/mf6/mfmodel.py b/flopy/mf6/mfmodel.py index b2089c52a5..9eb7c16d15 100644 --- a/flopy/mf6/mfmodel.py +++ b/flopy/mf6/mfmodel.py @@ -39,26 +39,29 @@ class MFModel(ModelInterface): Parameters ---------- - simulation_data : MFSimulationData - Simulation data object of the simulation this model will belong to + simulation : MFSimulation + Simulation object that this model is a part of. + model_type : str, default "gwf6" + Model type. + modelname : str, default "model" + Name of the model. + model_nam_file : str, optional + Relative path to the model name file from model working folder. + version : str, default "mf6" + Version of modflow. + exe_name : str, default "mf6" + Model executable name. + add_to_simulation : bool, default True + Adds model to simulation. structure : MFModelStructure - Structure of this type of model - modelname : str - Name of the model - model_nam_file : str - Relative path to the model name file from model working folder - version : str - Version of modflow - exe_name : str - Model executable name - model_ws : str - Model working folder path - disfile : str - Relative path to dis file from model working folder - grid_type : str - Type of grid the model will use (structured, unstructured, vertices) - verbose : bool - Verbose setting for model operations (default False) + Structure of this type of model. + model_rel_path : str or PathLike, default "." (curdir) + Relative path of model folder to simulation folder. + verbose : bool, default False + Verbose setting for model operations. + **kwargs + Extra keyword options to support spatial referencing, including + ``xll``, ``yll``, ``xul``, ``yul``, ``rotation``, and ``crs``. Attributes ---------- @@ -880,30 +883,30 @@ def load_base( Parameters ---------- simulation : MFSimulation - simulation object that this model is a part of - simulation_data : MFSimulationData - simulation data object + Simulation object that this model is a part of. structure : MFModelStructure - structure of this type of model - model_name : str - name of the model - model_nam_file : str - relative path to the model name file from model working folder - version : str - version of modflow + Structure of this type of model. + modelname : str, default "NewModel" + Name of the model. + model_nam_file : str, default "modflowtest.nam" + Relative path to the model name file from model working folder. + mtype : str, default "gwf" + Not used. + version : str, default "mf6" + Version of modflow. exe_name : str or PathLike, default "mf6" - model executable name or path - strict : bool - strict mode when loading files + Model executable name or path. + strict : bool, default True + Strict mode when loading files. model_rel_path : str, default "." (curdir) - relative path of model folder to simulation folder + relative path of model folder to simulation folder. load_only : list - list of package abbreviations or package names corresponding to + List of package abbreviations or package names corresponding to packages that flopy will load. default is None, which loads all - packages. the discretization packages will load regardless of this - setting. subpackages, like time series and observations, will also + packages. The discretization packages will load regardless of this + setting. Subpackages, like time series and observations, will also load regardless of this setting. - example list: ['ic', 'maw', 'npf', 'oc', 'my_well_package_1'] + Example list: ``['ic', 'maw', 'npf', 'oc', 'my_well_package_1']`` Returns ------- diff --git a/flopy/mf6/mfpackage.py b/flopy/mf6/mfpackage.py index c522e35712..cd60152d71 100644 --- a/flopy/mf6/mfpackage.py +++ b/flopy/mf6/mfpackage.py @@ -1694,18 +1694,20 @@ class MFPackage(PackageInterface): Parameters ---------- parent : MFModel, MFSimulation, or MFPackage - The parent model, simulation, or package containing this package + The parent model, simulation, or package containing this package. package_type : str - String defining the package type - filename : str or PathLike - Name or path of file where this package is stored + String defining the package type. + filename : str or PathLike, optional + Name or path of file where this package is stored. quoted_filename : str Filename with quotes around it when there is a space in the name - pname : str - Package name - loading_package : bool + pname : str, optional + Package name. + loading_package : bool, default False Whether or not to add this package to the parent container's package list during initialization + **kwargs + Extra keyword arguments. Attributes ---------- diff --git a/flopy/mf6/mfsimbase.py b/flopy/mf6/mfsimbase.py index 419fc4c086..e044419bca 100644 --- a/flopy/mf6/mfsimbase.py +++ b/flopy/mf6/mfsimbase.py @@ -404,48 +404,51 @@ class MFSimulationBase: Parameters ---------- - sim_name : str + sim_name : str, default "sim" Name of the simulation. - version : str - Version of MODFLOW 6 executable - exe_name : str - Path to MODFLOW 6 executable - sim_ws : str + version : str, default "mf6" + Version of MODFLOW 6 executable. + exe_name : str, default "mf6" + Path to MODFLOW 6 executable. + sim_ws : str or PathLike, default ".' (curdir) Path to MODFLOW 6 simulation working folder. This is the folder containing the simulation name file. - verbosity_level : int - Verbosity level of standard output from 0 to 2. When 0 is specified no - standard output is written. When 1 is specified standard - error/warning messages with some informational messages are written. - When 2 is specified full error/warning/informational messages are - written (this is ideal for debugging). - continue_ : bool + verbosity_level : int, default 1 + Verbosity level of standard output: + + 0. No standard output + 1. Standard error/warning messages with some informational + messages + 2. Verbose mode with full error/warning/informational messages. + This is ideal for debugging. + continue_ : bool, optional Sets the continue option in the simulation name file. The continue option is a keyword flag to indicate that the simulation should continue even if one or more solutions do not converge. - nocheck : bool - Sets the nocheck option in the simulation name file. The nocheck - option is a keyword flag to indicate that the model input check - routines should not be called prior to each time step. Checks - are performed by default. - memory_print_option : str - Sets memory_print_option in the simulation name file. - Memory_print_option is a flag that controls printing of detailed - memory manager usage to the end of the simulation list file. NONE - means do not print detailed information. SUMMARY means print only - the total memory for each simulation component. ALL means print - information for each variable stored in the memory manager. NONE is - default if memory_print_option is not specified. - write_headers: bool - When true flopy writes a header to each package file indicating that + nocheck : bool, optional + Sets the nocheck option in the simulation name file. The nocheck + option is a keyword flag to indicate that the model input check + routines should not be called prior to each time step. Checks + are performed by default. + memory_print_option : str, optional + Sets memory_print_option in the simulation name file. + Memory_print_option is a flag that controls printing of detailed + memory manager usage to the end of the simulation list file. None + means do not print detailed information. SUMMARY means print only + the total memory for each simulation component. ALL means print + information for each variable stored in the memory manager. None is + default if memory_print_option is not specified. + write_headers: bool, default True + When True flopy writes a header to each package file indicating that it was created by flopy. - lazy_io: bool - When true flopy only reads external data when the data is requested + lazy_io: bool, default False + When True flopy only reads external data when the data is requested and only writes external data if the data has changed. This option automatically overrides the verify_data and auto_set_sizes, turning both off. - use_pandas: bool - Load/save data using pandas dataframes (for supported data) + use_pandas: bool, default True + Load/save data using pandas dataframes (for supported data). + Examples -------- >>> s = MFSimulationBase.load('my simulation', 'simulation.nam') @@ -779,48 +782,48 @@ def load( Parameters ---------- - cls_child : - cls object of child class calling load - sim_name : str + sim_name : str, default "modflowsim" Name of the simulation. - version : str - MODFLOW version - exe_name : str or PathLike - Path to MODFLOW executable (relative to the simulation workspace or absolute) - sim_ws : str or PathLike - Path to simulation workspace - strict : bool - Strict enforcement of file formatting - verbosity_level : int - Verbosity level of standard output - 0: No standard output - 1: Standard error/warning messages with some informational + version : str, default "mf6" + Version of MODFLOW 6 executable. + exe_name : str or PathLike, default "mf6" + Path to MODFLOW 6 executable. + sim_ws : str or PathLike, default "." (curdir) + Path to MODFLOW 6 simulation working folder. This is the folder + containing the simulation name file. + strict : bool, default True + Strict enforcement of file formatting. + verbosity_level : int, default 1 + Verbosity level of standard output: + + 0. No standard output + 1. Standard error/warning messages with some informational messages - 2: Verbose mode with full error/warning/informational - messages. This is ideal for debugging. - load_only : list + 2. Verbose mode with full error/warning/informational messages. + This is ideal for debugging. + load_only : list, optional List of package abbreviations or package names corresponding to packages that flopy will load. default is None, which loads all packages. the discretization packages will load regardless of this setting. subpackages, like time series and observations, will also load regardless of this setting. example list: ['ic', 'maw', 'npf', 'oc', 'ims', 'gwf6-gwf6'] - verify_data : bool - Verify data when it is loaded. this can slow down loading - write_headers: bool - When true flopy writes a header to each package file indicating - that it was created by flopy - lazy_io: bool - When true flopy only reads external data when the data is requested + verify_data : bool, default False + Verify data when it is loaded. This can slow down loading. + write_headers: bool, default True + When True flopy writes a header to each package file indicating + that it was created by flopy. + lazy_io: bool, default False + When True flopy only reads external data when the data is requested and only writes external data if the data has changed. This option automatically overrides the verify_data and auto_set_sizes, turning both off. - use_pandas: bool - Load/save data using pandas dataframes (for supported data) + use_pandas: bool, default True + Load/save data using pandas dataframes (for supported data). Returns ------- - sim : MFSimulation object + MFSimulation object Examples -------- diff --git a/flopy/mf6/utils/codegen/__init__.py b/flopy/mf6/utils/codegen/__init__.py index b2d9f69b3f..187fe23c68 100644 --- a/flopy/mf6/utils/codegen/__init__.py +++ b/flopy/mf6/utils/codegen/__init__.py @@ -18,26 +18,26 @@ def _get_template_env(): keep_trailing_newline=True, ) - from flopy.mf6.utils.codegen.filters import Filters - - env.filters["base"] = Filters.base - env.filters["title"] = Filters.title - env.filters["description"] = Filters.description - env.filters["prefix"] = Filters.prefix - env.filters["parent"] = Filters.parent - env.filters["skip_init"] = Filters.skip_init - env.filters["package_abbr"] = Filters.package_abbr - env.filters["variables"] = Filters.variables - env.filters["attrs"] = Filters.attrs - env.filters["init"] = Filters.init - env.filters["untag"] = Filters.untag - env.filters["type"] = Filters.type - env.filters["children"] = Filters.children - env.filters["default_value"] = Filters.default_value - env.filters["safe_name"] = Filters.safe_name - env.filters["value"] = Filters.value - env.filters["math"] = Filters.math - env.filters["clean"] = Filters.clean + from flopy.mf6.utils.codegen import filters + + env.filters["base"] = filters.base + env.filters["title"] = filters.title + env.filters["description"] = filters.description + env.filters["prefix"] = filters.prefix + env.filters["parent"] = filters.parent + env.filters["skip_init"] = filters.skip_init + env.filters["package_abbr"] = filters.package_abbr + env.filters["variables"] = filters.variables + env.filters["attrs"] = filters.attrs + env.filters["init"] = filters.init + env.filters["untag"] = filters.untag + env.filters["type"] = filters.type + env.filters["children"] = filters.children + env.filters["default_value"] = filters.default_value + env.filters["safe_name"] = filters.safe_name + env.filters["value"] = filters.value + env.filters["math"] = filters.math + env.filters["clean"] = filters.clean return env @@ -72,11 +72,11 @@ def make_targets(dfn, outdir: PathLike, verbose: bool = False): # import here instead of module so we don't # expect optional deps at module init time + from flopy.mf6.utils.codegen import filters from flopy.mf6.utils.codegen.component import ComponentDescriptor - from flopy.mf6.utils.codegen.filters import Filters def _get_template_name(component_name) -> str: - base = Filters.base(component_name) + base = filters.base(component_name) if base == "MFSimulationBase": return "simulation.py.jinja" elif base == "MFModel": @@ -88,9 +88,12 @@ def _get_template_name(component_name) -> str: else: raise NotImplementedError(f"Unknown base class: {base}") + if verbose: + print(f"Making target for DFN {dfn['name']!r} ...") + for component in ComponentDescriptor.from_dfn(dfn): component_name = component["name"] - target_path = outdir / f"mf{Filters.title(component_name)}.py" + target_path = outdir / f"mf{filters.title(component_name)}.py" template = env.get_template(_get_template_name(component_name)) with open(target_path, "w") as f: f.write(template.render(**component)) diff --git a/flopy/mf6/utils/codegen/filters.py b/flopy/mf6/utils/codegen/filters.py index 35c5849828..fc431cc318 100644 --- a/flopy/mf6/utils/codegen/filters.py +++ b/flopy/mf6/utils/codegen/filters.py @@ -34,528 +34,526 @@ def enter(p, k, v): return vars_ -class Filters: - - def base(component_name: tuple[str, str]) -> str: - """Base class from which the input context should inherit.""" - if component_name == ("sim", "nam"): - return "MFSimulationBase" - if component_name[1] is None: - return "MFModel" - return "MFPackage" - - def title(component_name: tuple[str, str]) -> str: - """ - The input context's unique title. This is not - identical to `f"{l}{r}` in some cases, but it - remains unique. The title is substituted into - the file name and class name. - """ - if component_name == ("sim", "nam"): - return "simulation" - l, r = component_name - if l is None: - return r - if r is None: - return l - if l == "sim": - return r - if l in ["sln", "exg"]: - return r - return l + r - - def package_abbr(component_name: tuple[str, str]) -> str: - if component_name[0] in ["sim", "sln", "exg", None]: - return component_name[1] - return "".join(component_name) - - def description(component_name: tuple[str, str]) -> str: - """A description of the input context.""" - l, r = component_name - base = Filters.base(component_name) - title = Filters.title(component_name).title() - if base == "MFPackage": - return f"Modflow{title} defines a {r.upper()} package." - elif base == "MFModel": - return f"Modflow{title} defines a {l.upper()} model." - elif base == "MFSimulationBase": - return ( - "MFSimulation is used to load, build, and/or save a MODFLOW 6 simulation." - " A MFSimulation object must be created before creating any of the MODFLOW" - " 6 model objects." - ) - - def prefix(component_name: tuple[str, str]) -> str: - """The input context class name prefix, e.g. 'MF' or 'Modflow'.""" - base = Filters.base(component_name) - return "MF" if base == "MFSimulationBase" else "Modflow" - - def dfn_file_name(component_name: tuple[str, str]) -> str: - if component_name[0] == "exg": - return f"{'-'.join(component_name)}.dfn" - if tuple(component_name) in [ - (None, "mvr"), - (None, "gnc"), - ]: - return f"gwf-{component_name[1]}.dfn" - if tuple(component_name) in [ - (None, "mvt") - ]: - return f"gwt-{component_name[1]}.dfn" - return f"{component_name[0] or 'sim'}-{component_name[1]}.dfn" - - def parent(dfn: dict, component_name: tuple[str, str]) -> str: - # TODO should be no longer needed when parents are explicit in dfns - """The input context's parent context type, if it can have a parent.""" - subpkg = dfn.get("ref", None) - if subpkg: - return subpkg["parent"] - if component_name == ("sim", "nam"): - return None - elif ( - component_name[1] is None - or component_name[0] in [None, "sim", "exg", "sln"] - ): - return "simulation" - return "model" +def base(component_name: tuple[str, str]) -> str: + """Base class from which the input context should inherit.""" + if component_name == ("sim", "nam"): + return "MFSimulationBase" + if component_name[1] is None: + return "MFModel" + return "MFPackage" + +def title(component_name: tuple[str, str]) -> str: + """ + The input context's unique title. This is not + identical to `f"{l}{r}` in some cases, but it + remains unique. The title is substituted into + the file name and class name. + """ + if component_name == ("sim", "nam"): + return "simulation" + l, r = component_name + if l is None: + return r + if r is None: + return l + if l == "sim": + return r + if l in ["sln", "exg"]: + return r + return l + r + +def package_abbr(component_name: tuple[str, str]) -> str: + if component_name[0] in ["sim", "sln", "exg", None]: + return component_name[1] + return "".join(component_name) + +def description(component_name: tuple[str, str]) -> str: + """A description of the input context.""" + l, r = component_name + component_base = base(component_name) + component_title = title(component_name).title() + if component_base == "MFPackage": + return f"Modflow{component_title} defines a {r.upper()} package." + elif component_base == "MFModel": + return f"Modflow{component_title} defines a {l.upper()} model." + elif component_base == "MFSimulationBase": + return ( + "MFSimulation is used to load, build, and/or save a MODFLOW 6 simulation." + " A MFSimulation object must be created before creating any of the MODFLOW" + " 6 model objects." + ) + +def prefix(component_name: tuple[str, str]) -> str: + """The input context class name prefix, e.g. 'MF' or 'Modflow'.""" + component_base = base(component_name) + return "MF" if component_base == "MFSimulationBase" else "Modflow" + +def dfn_file_name(component_name: tuple[str, str]) -> str: + if component_name[0] == "exg": + return f"{'-'.join(component_name)}.dfn" + if tuple(component_name) in [ + (None, "mvr"), + (None, "gnc"), + ]: + return f"gwf-{component_name[1]}.dfn" + if tuple(component_name) in [ + (None, "mvt") + ]: + return f"gwt-{component_name[1]}.dfn" + return f"{component_name[0] or 'sim'}-{component_name[1]}.dfn" + +def parent(dfn: dict, component_name: tuple[str, str]) -> str: + # TODO should be no longer needed when parents are explicit in dfns + """The input context's parent context type, if it can have a parent.""" + subpkg = dfn.get("ref", None) + if subpkg: + return subpkg["parent"] + if component_name == ("sim", "nam"): + return None + elif ( + component_name[1] is None + or component_name[0] in [None, "sim", "exg", "sln"] + ): + return "simulation" + return "model" + +def skip_init(component_name: tuple[str, str]) -> List[str]: + """Variables to skip in input context's `__init__` method.""" + component_base = base(component_name) + if component_base == "MFSimulationBase": + return [ + "tdis6", + "models", + "exchanges", + "mxiter", + "solutiongroup", + ] + elif component_base == "MFModel": + return ["packages"] + else: + # if component_name[1] == "nam": + # return ["export_netcdf", "nc_filerecord"] + if component_name == ("utl", "ts"): + return ["method", "interpolation_method_single", "sfac"] + return [] + +def untag(var: dict) -> dict: + """ + If the variable is a tagged record, remove the leading + tag field. If the variable is a tagged file path input + record, remove both leading tag and 'filein'/'fileout' + keyword following it. + """ + name = var["name"] + tagged = var.get("tagged", False) + fields = var.get("fields", None) - def skip_init(component_name: tuple[str, str]) -> List[str]: - """Variables to skip in input context's `__init__` method.""" - base = Filters.base(component_name) - if base == "MFSimulationBase": - return [ - "tdis6", - "models", - "exchanges", - "mxiter", - "solutiongroup", - ] - elif base == "MFModel": - return ["packages"] - else: - # if component_name[1] == "nam": - # return ["export_netcdf", "nc_filerecord"] - if component_name == ("utl", "ts"): - return ["method", "interpolation_method_single", "sfac"] - return [] - - def untag(var: dict) -> dict: - """ - If the variable is a tagged record, remove the leading - tag field. If the variable is a tagged file path input - record, remove both leading tag and 'filein'/'fileout' - keyword following it. - """ - name = var["name"] - tagged = var.get("tagged", False) - fields = var.get("fields", None) - - if not fields: - return var - - # if tagged, remove the leading keyword - elif tagged: - keyword = next(iter(fields), None) - if keyword: - fields.pop(keyword) - - # if the record represents a file... - elif "file" in name: - # remove filein/fileout - field_names = list(fields.keys()) - for term in ["filein", "fileout"]: - if term in field_names: - fields.pop(term) - - # remove leading keyword - keyword = next(iter(fields), None) - if keyword: - fields.pop(keyword) - - var["fields"] = fields + if not fields: return var - def type(var: dict) -> str: - """ - Get a readable representation of the variable's type. - TODO: eventually replace this with a proper `type` in - the variable spec when we add type hints - """ - _type = var["type"] - shape = var.get("shape", None) - children = Filters.children(var) - if children: - if _type == "recarray": - if len(children) == 1: - first = list(children.values())[0] - if first["type"] in ["record", "keystring"]: - return f"[{Filters.type(first)}]" - children = ", ".join( - [v["name"] for v in children.values()] - ) - return f"[{children}]" - elif _type == "record": - children = ", ".join( - [v["name"] for v in children.values()] - ) - return f"({children})" - elif _type == "keystring": - return " | ".join([v["name"] for v in children.values()]) - elif shape: - return f"[{_type}]" - return var["type"] - - def children(var: dict) -> Optional[dict]: - _type = var["type"] - items = var.get("items", None) - fields = var.get("fields", None) - choices = var.get("choices", None) - if items: - assert _type == "recarray" - return items - if fields: - assert _type == "record" - return fields - if choices: - assert _type == "keystring" - return choices - return None - - def default_value(var: dict) -> Any: - _default = var.get("default", None) - if _default is not None: - return _default - return None + # if tagged, remove the leading keyword + elif tagged: + keyword = next(iter(fields), None) + if keyword: + fields.pop(keyword) + + # if the record represents a file... + elif "file" in name: + # remove filein/fileout + field_names = list(fields.keys()) + for term in ["filein", "fileout"]: + if term in field_names: + fields.pop(term) + + # remove leading keyword + keyword = next(iter(fields), None) + if keyword: + fields.pop(keyword) + + var["fields"] = fields + return var + +def type(var: dict) -> str: + """ + Get a readable representation of the variable's type. + TODO: eventually replace this with a proper `type` in + the variable spec when we add type hints + """ + _type = var["type"] + shape = var.get("shape", None) + children_vars = children(var) + if children_vars: + if _type == "recarray": + if len(children_vars) == 1: + first = list(children_vars.values())[0] + if first["type"] in ["record", "keystring"]: + return f"[{type(first)}]" + children_vars = ", ".join( + [v["name"] for v in children_vars.values()] + ) + return f"[{children_vars}]" + elif _type == "record": + children_vars = ", ".join( + [v["name"] for v in children_vars.values()] + ) + return f"({children_vars})" + elif _type == "keystring": + return " | ".join([v["name"] for v in children_vars.values()]) + elif shape: + return f"[{_type}]" + return var["type"] + +def children(var: dict) -> Optional[dict]: + _type = var["type"] + assert "items" not in var, 'TODO: fix bug to use "item" (not "items")' + items = var.get("items", None) # TODO: item = var.get("item", None) + fields = var.get("fields", None) + choices = var.get("choices", None) + if items: + assert _type == "recarray" + return items + if fields: + assert _type == "record" + return fields + if choices: + assert _type == "keystring" + return choices + return None + +def default_value(var: dict) -> Any: + _default = var.get("default", None) + if _default is not None: + return _default + return None + +def variables(dfn: dict) -> List[str]: + return _get_vars(dfn) + +def attrs(dfn: dict, component_name: tuple[str, str]) -> List[str]: + """ + Map the context's input variables to corresponding class attributes, + where applicable. TODO: this should get much simpler if we can drop + all the `ListTemplateGenerator`/`ArrayTemplateGenerator` attributes. + """ + from modflow_devtools.dfn import _SCALAR_TYPES + + component_base = base(component_name) + component_vars = _get_vars(dfn) + + def _attr(var: dict) -> Optional[str]: + var_name = var["name"] + var_type = var["type"] + var_block = var["block"] + var_shape = var.get("shape", None) + var_subpkg = var.get("ref", None) + + if ( + (var_type in _SCALAR_TYPES and not var_shape) + or var_name in ["cvoptions", "output"] + # or (component_name[1] == "dis" and var_name == "packagedata") + ): + return None - def variables(dfn: dict) -> List[str]: - return _get_vars(dfn) - - def attrs(dfn: dict, component_name: tuple[str, str]) -> List[str]: - """ - Map the context's input variables to corresponding class attributes, - where applicable. TODO: this should get much simpler if we can drop - all the `ListTemplateGenerator`/`ArrayTemplateGenerator` attributes. - """ - from modflow_devtools.dfn import _SCALAR_TYPES - - component_base = Filters.base(component_name) - component_vars = _get_vars(dfn) - - def _attr(var: dict) -> Optional[str]: - var_name = var["name"] - var_type = var["type"] - var_block = var["block"] - var_shape = var.get("shape", None) - var_subpkg = var.get("ref", None) - - if ( - (var_type in _SCALAR_TYPES and not var_shape) - or var_name in ["cvoptions", "output"] - # or (component_name[1] == "dis" and var_name == "packagedata") - ): - return None - - if var_subpkg: - # if the variable is a subpackage reference, use the original key - # (which has been replaced already with the referenced variable) + if var_subpkg: + # if the variable is a subpackage reference, use the original key + # (which has been replaced already with the referenced variable) + args = [ + f"'{component_name[1]}'", + f"'{var_block}'", + f"'{var_subpkg['key']}'", + ] + if component_name[0] not in [ + None, + "sim", + "sln", + "utl", + "exg", + ]: + args.insert(0, f"'{component_name[0]}6'") + return f"{var_subpkg['key']} = ListTemplateGenerator(({', '.join(args)}))" + is_array = ( + var_type in ["string", "integer", "double precision"] + and var_shape + ) + is_composite = var_type in ["recarray", "record", "keystring"] + if is_array or is_composite: + def _args(): args = [ f"'{component_name[1]}'", f"'{var_block}'", - f"'{var_subpkg['key']}'", + f"'{var_name}'", ] - if component_name[0] not in [ - None, + if component_name[0] is not None and component_name[0] not in [ "sim", "sln", "utl", "exg", ]: args.insert(0, f"'{component_name[0]}6'") - return f"{var_subpkg['key']} = ListTemplateGenerator(({', '.join(args)}))" - is_array = ( - var_type in ["string", "integer", "double precision"] - and var_shape - ) - is_composite = var_type in ["recarray", "record", "keystring"] - if is_array or is_composite: - def _args(): - args = [ - f"'{component_name[1]}'", - f"'{var_block}'", - f"'{var_name}'", - ] - if component_name[0] is not None and component_name[0] not in [ - "sim", - "sln", - "utl", - "exg", - ]: - args.insert(0, f"'{component_name[0]}6'") - return args - - kind = "array" if is_array else "list" - return f"{var_name} = {kind.title()}TemplateGenerator(({', '.join(_args())}))" + return args - return None + kind = "array" if is_array else "list" + return f"{var_name} = {kind.title()}TemplateGenerator(({', '.join(_args())}))" - attrs = list(filter(None, [_attr(v) for v in component_vars.values()])) + return None + + attrs = list(filter(None, [_attr(v) for v in component_vars.values()])) - dfn_file_name = Filters.dfn_file_name(component_name) - dfn_header = ["header"] - if dfn.get("multi", None): - dfn_header.append("multi-package") - if dfn.get("advanced", None): - dfn_header.append("package-type advanced-stress-package") - if dfn.get("sln", None): - dfn_header.append(["solution_package", "*"]) + dfn_header = ["header"] + if dfn.get("multi", None): + dfn_header.append("multi-package") + if dfn.get("advanced", None): + dfn_header.append("package-type advanced-stress-package") + if dfn.get("sln", None): + dfn_header.append(["solution_package", "*"]) - dfn_dir = Path(__file__).parents[2] / "data" / "dfn" + dfn_dir = Path(__file__).parents[2] / "data" / "dfn" - def _dfn(definition, metadata) -> List[List[str]]: - def _meta(): - exclude = ["subpackage", "parent_name_type"] + def _dfn(definition, metadata) -> List[List[str]]: + def _meta(): + exclude = ["subpackage", "parent_name_type"] + return [ + v for v in metadata if not any(p in v for p in exclude) + ] + + def __dfn(): + def _var(var: dict) -> List[str]: + exclude = ["longname", "description"] + name = var["name"] + subpkg = dfn.get("fkeys", dict()).get(name, None) + if subpkg: + var["construct_package"] = subpkg["abbr"] + var["construct_data"] = subpkg["val"] + var["parameter_name"] = subpkg["param"] return [ - v for v in metadata if not any(p in v for p in exclude) + " ".join([k, v]).strip() + for k, v in var.items() + if k not in exclude ] - def __dfn(): - def _var(var: dict) -> List[str]: - exclude = ["longname", "description"] - name = var["name"] - subpkg = dfn.get("fkeys", dict()).get(name, None) - if subpkg: - var["construct_package"] = subpkg["abbr"] - var["construct_data"] = subpkg["val"] - var["parameter_name"] = subpkg["param"] - return [ - " ".join([k, v]).strip() - for k, v in var.items() - if k not in exclude - ] - - return [_var(var) for var in list(definition.values(multi=True))] - - return [["header"] + _meta()] + __dfn() - - def _filter_metadata(metadata): - meta_ = list() - for m in metadata: - if "multi" in m: - meta_.append(m) - elif "solution" in m: - s = m.split() - meta_.append([s[0], s[2]]) - elif "package-type" in m: - s = m.split() - meta_.append(" ".join(s)) - return meta_ - - legacy_dfn = dfn.get("legacy_dfn", {}) - legacy_meta = dfn.get("legacy_meta", []) - legacy_dfn = _dfn(legacy_dfn, _filter_metadata(legacy_meta)) - if component_base == "MFPackage": - attrs.extend( - [ - f"package_abbr = '{Filters.package_abbr(component_name)}'", - f"_package_type = '{component_name[1]}'", - f"dfn_file_name = '{dfn_file_name}'", - f"dfn = {pformat(legacy_dfn, indent=10, width=sys.maxsize)}" - ] - ) + return [_var(var) for var in list(definition.values(multi=True))] + + return [["header"] + _meta()] + __dfn() + + def _filter_metadata(metadata): + meta_ = list() + for m in metadata: + if "multi" in m: + meta_.append(m) + elif "solution" in m: + s = m.split() + meta_.append([s[0], s[2]]) + elif "package-type" in m: + s = m.split() + meta_.append(" ".join(s)) + return meta_ + + legacy_dfn = dfn.get("legacy_dfn", {}) + legacy_meta = dfn.get("legacy_meta", []) + legacy_dfn = _dfn(legacy_dfn, _filter_metadata(legacy_meta)) + if component_base == "MFPackage": + attrs.extend( + [ + f"package_abbr = '{package_abbr(component_name)}'", + f"_package_type = '{component_name[1]}'", + f"dfn_file_name = '{dfn_file_name(component_name)}'", + f"dfn = {pformat(legacy_dfn, indent=10, width=sys.maxsize)}" + ] + ) - return attrs - - def init(dfn: dict, component_name: tuple[str, str]) -> List[str]: - component_base = Filters.base(component_name) - component_vars = _get_vars(dfn) - - def _statements() -> Optional[List[str]]: - if component_base == "MFSimulationBase": - - def _should_set(var: dict) -> bool: - return var["name"] not in [ - "tdis6", - "models", - "exchanges", - "mxiter", - "solutiongroup", - ] - - stmts = [] - refs = {} - for var in component_vars.values(): - name = var["name"] - if name in kwlist: - name = f"{name}_" - - subpkg = var.get("ref", None) - - if _should_set(var): - if name not in ["hpc_data"]: - stmts.append( - f"self.name_file.{name}.set_data({name})" - ) - if not subpkg: - stmts.append( - f"self.{name} = self.name_file.{name}" - ) - - if subpkg and subpkg["key"] not in refs: - refs[subpkg["key"]] = subpkg - args = f"'{subpkg['abbr']}', {subpkg['param']}" - stmts.append( - f"self.{subpkg['param']} = self._create_package({args})" - ) - elif component_base == "MFModel": + return attrs + +def init(dfn: dict, component_name: tuple[str, str]) -> List[str]: + component_base = base(component_name) + component_vars = _get_vars(dfn) + + def _statements() -> Optional[List[str]]: + if component_base == "MFSimulationBase": + + def _should_set(var: dict) -> bool: + return var["name"] not in [ + "tdis6", + "models", + "exchanges", + "mxiter", + "solutiongroup", + ] - def _should_set(var: dict) -> bool: - return var["name"] not in [ - "packages", - ] + stmts = [] + refs = {} + for var in component_vars.values(): + name = var["name"] + if name in kwlist: + name = f"{name}_" - stmts = [] - refs = {} - for var in component_vars.values(): - name = var["name"] - if name in kwlist: - name = f"{name}_" + subpkg = var.get("ref", None) - if _should_set(var): + if _should_set(var): + if name not in ["hpc_data"]: stmts.append( f"self.name_file.{name}.set_data({name})" ) + if not subpkg: stmts.append( f"self.{name} = self.name_file.{name}" ) - subpkg = var.get("ref", None) - if subpkg and subpkg["key"] not in refs: - refs[subpkg["key"]] = subpkg - args = f"'{subpkg['abbr']}', {subpkg['param']}" - stmts.append( - f"self.{subpkg['param']} = self._create_package({args})" - ) - elif component_base == "MFPackage": - - def _should_build(var: dict) -> bool: - subpkg = var.get("ref", None) - if subpkg and component_name != (None, "nam"): - return False - return var["name"] not in [ - "simulation", - "model", - "package", - "parent_model", - "parent_package", - "parent_model_or_package", - "parent_file", - "modelname", - "model_nam_file", - "method", - "interpolation_method_single", - "sfac", - "output", - ] - - stmts = [] - refs = {} - for var in component_vars.values(): - name = var["name"] - if name in kwlist: - name = f"{name}_" - - subpkg = var.get("ref", None) - if _should_build(var): - if subpkg and component_name == (None, "nam"): - stmts.append( - f"self.{'_' if subpkg else ''}{subpkg['key']} " - f"= self.build_mfdata('{subpkg['key']}', None)" - ) - else: - _name = ( - name[:-1] if name.endswith("_") else name - ) - name = name.replace("-", "_") - stmts.append( - f"self.{'_' if subpkg else ''}{name} " - f"= self.build_mfdata('{_name}', {name})" - ) - - if ( - subpkg - and subpkg["key"] not in refs - and component_name[1] != "nam" - ): - refs[subpkg["key"]] = subpkg + if subpkg and subpkg["key"] not in refs: + refs[subpkg["key"]] = subpkg + args = f"'{subpkg['abbr']}', {subpkg['param']}" + stmts.append( + f"self.{subpkg['param']} = self._create_package({args})" + ) + elif component_base == "MFModel": + + def _should_set(var: dict) -> bool: + return var["name"] not in [ + "packages", + ] + + stmts = [] + refs = {} + for var in component_vars.values(): + name = var["name"] + if name in kwlist: + name = f"{name}_" + + if _should_set(var): + stmts.append( + f"self.name_file.{name}.set_data({name})" + ) + stmts.append( + f"self.{name} = self.name_file.{name}" + ) + + subpkg = var.get("ref", None) + if subpkg and subpkg["key"] not in refs: + refs[subpkg["key"]] = subpkg + args = f"'{subpkg['abbr']}', {subpkg['param']}" + stmts.append( + f"self.{subpkg['param']} = self._create_package({args})" + ) + elif component_base == "MFPackage": + + def _should_build(var: dict) -> bool: + subpkg = var.get("ref", None) + if subpkg and component_name != (None, "nam"): + return False + return var["name"] not in [ + "simulation", + "model", + "package", + "parent_model", + "parent_package", + "parent_model_or_package", + "parent_file", + "modelname", + "model_nam_file", + "method", + "interpolation_method_single", + "sfac", + "output", + ] + + stmts = [] + refs = {} + for var in component_vars.values(): + name = var["name"] + if name in kwlist: + name = f"{name}_" + + subpkg = var.get("ref", None) + if _should_build(var): + if subpkg and component_name == (None, "nam"): stmts.append( - f"self._{subpkg['key']} " + f"self.{'_' if subpkg else ''}{subpkg['key']} " f"= self.build_mfdata('{subpkg['key']}', None)" ) - args = ( - f"'{subpkg['abbr']}', {subpkg['val']}, " - f"'{subpkg['param']}', self._{subpkg['key']}" + else: + _name = ( + name[:-1] if name.endswith("_") else name ) + name = name.replace("-", "_") stmts.append( - f"self._{subpkg['abbr']}_package " - f"= self.build_child_package({args})" + f"self.{'_' if subpkg else ''}{name} " + f"= self.build_mfdata('{_name}', {name})" ) - return stmts - - return list(filter(None, _statements())) - - def safe_name(v: str) -> str: - """ - Make sure a string is safe to use as a variable name in Python code. - If the string is a reserved keyword, add a trailing underscore to it. - Also replace any hyphens with underscores. - """ - return (f"{v}_" if v in kwlist else v).replace("-", "_") - - def math(v: str) -> str: - """Massage latex equations""" - v = v.replace("$<$", "<") - v = v.replace("$>$", ">") - if "$" in v: - descsplit = v.split("$") - mylist = [ - i.replace("\\", "") - + ":math:`" - + j.replace("\\", "\\\\") - + "`" - for i, j in zip(descsplit[::2], descsplit[1::2]) - ] - mylist.append(descsplit[-1].replace("\\", "")) - v = "".join(mylist) - else: - v = v.replace("\\", "") - return v - - def clean(v: str) -> str: - """Clean description""" - replace_pairs = [ - ("``", '"'), # double quotes - ("''", '"'), - ("`", "'"), # single quotes - ("~", " "), # non-breaking space - (r"\mf", "MODFLOW 6"), - (r"\citep{konikow2009}", "(Konikow et al., 2009)"), - (r"\citep{hill1990preconditioned}", "(Hill, 1990)"), - (r"\ref{table:ftype}", "in mf6io.pdf"), - (r"\ref{table:gwf-obstypetable}", "in mf6io.pdf"), + if ( + subpkg + and subpkg["key"] not in refs + and component_name[1] != "nam" + ): + refs[subpkg["key"]] = subpkg + stmts.append( + f"self._{subpkg['key']} " + f"= self.build_mfdata('{subpkg['key']}', None)" + ) + args = ( + f"'{subpkg['abbr']}', {subpkg['val']}, " + f"'{subpkg['param']}', self._{subpkg['key']}" + ) + stmts.append( + f"self._{subpkg['abbr']}_package " + f"= self.build_child_package({args})" + ) + + return stmts + + return list(filter(None, _statements())) + +def safe_name(v: str) -> str: + """ + Make sure a string is safe to use as a variable name in Python code. + If the string is a reserved keyword, add a trailing underscore to it. + Also replace any hyphens with underscores. + """ + return (f"{v}_" if v in kwlist else v).replace("-", "_") + +def math(v: str) -> str: + """Massage latex equations""" + v = v.replace("$<$", "<") + v = v.replace("$>$", ">") + if "$" in v: + descsplit = v.split("$") + mylist = [ + i.replace("\\", "") + + ":math:`" + + j.replace("\\", "\\\\") + + "`" + for i, j in zip(descsplit[::2], descsplit[1::2]) ] - for s1, s2 in replace_pairs: - if s1 in v: - v = v.replace(s1, s2) - return v - - def value(v: Any) -> str: - """ - Format a value to appear in the RHS of an assignment or argument- - passing expression: if it's an enum, get its value; if it's `str`, - quote it. - """ - v = _try_get_enum_value(v) - if isinstance(v, str) and v[0] not in ["'", '"']: - v = f"'{v}'" - return v + mylist.append(descsplit[-1].replace("\\", "")) + v = "".join(mylist) + else: + v = v.replace("\\", "") + return v + +def clean(v: str) -> str: + """Clean description""" + replace_pairs = [ + ("``", '"'), # double quotes + ("''", '"'), + ("`", "'"), # single quotes + ("~", " "), # non-breaking space + (r"\mf", "MODFLOW 6"), + (r"\citep{konikow2009}", "(Konikow et al., 2009)"), + (r"\citep{hill1990preconditioned}", "(Hill, 1990)"), + (r"\ref{table:ftype}", "in mf6io.pdf"), + (r"\ref{table:gwf-obstypetable}", "in mf6io.pdf"), + ] + for s1, s2 in replace_pairs: + if s1 in v: + v = v.replace(s1, s2) + return v + +def value(v: Any) -> str: + """ + Format a value to appear in the RHS of an assignment or argument- + passing expression: if it's an enum, get its value; if it's `str`, + quote it. + """ + v = _try_get_enum_value(v) + if isinstance(v, str) and v[0] not in ["'", '"']: + v = f"'{v}'" + return v diff --git a/flopy/mf6/utils/codegen/templates/exchange.py.jinja b/flopy/mf6/utils/codegen/templates/exchange.py.jinja index c79454978d..e8d9a36295 100644 --- a/flopy/mf6/utils/codegen/templates/exchange.py.jinja +++ b/flopy/mf6/utils/codegen/templates/exchange.py.jinja @@ -17,7 +17,26 @@ class Modflow{{ title }}(MFPackage): Parameters ---------- + simulation : MFSimulation + Simulation that this package is a part of. Package is automatically + added to simulation when it is initialized. + loading_package : bool, default False + Do not set this parameter. It is intended for debugging and internal + processing purposes only. + exgtype : str, default "{{ name[1][:3].upper() }}6-{{ name[1][3:].upper() }}6" + The exchange type (GWF-GWF or GWF-GWT). + exgmnamea : str, optional + The name of the first model that is part of this exchange. + exgmnameb : str, optional + The name of the second model that is part of this exchange. {{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + filename : str or PathLike, optional + Name or path of file where this package is stored. + pname : str, optional + Package name. + **kwargs + Extra keywords for :class:`flopy.mf6.mfpackage.MFPackage`. + """ {% for attr in dfn|attrs(name) %} @@ -36,41 +55,14 @@ class Modflow{{ title }}(MFPackage): pname=None, **kwargs, ): - """ - {{ description }} - - simulation : MFSimulation - Simulation that this package is a part of. Package is automatically - added to simulation when it is initialized. - loading_package : bool - Do not set this parameter. It is intended for debugging and internal - processing purposes only. - exgtype : str - The exchange type (GWF-GWF or GWF-GWT). - exgmnamea : str - The name of the first model that is part of this exchange. - exgmnameb : str - The name of the second model that is part of this exchange. - gwfmodelname1 : str - Name of first GWF Model. In the simulation name file, the GWE6-GWE6 - entry contains names for GWE Models (exgmnamea and exgmnameb). The - GWE Model with the name exgmnamea must correspond to the GWF Model - with the name gwfmodelname1. - gwfmodelname2 : str - Name of second GWF Model. In the simulation name file, the GWE6-GWE6 - entry contains names for GWE Models (exgmnamea and exgmnameb). The - GWE Model with the name exgmnameb must correspond to the GWF Model - with the name gwfmodelname2. -{{ macros.docstrings(dfn|variables)|indent(8, first=true) }} - """ - + """Initialize Modflow{{ title }}.""" super().__init__( - {{ parent }}, - "{{ name[1] }}", - filename, - pname, - loading_package, - **kwargs + parent={{ parent }}, + package_type="{{ name[1] }}", + filename=filename, + pname=pname, + loading_package=loading_package, + **kwargs, ) self.exgtype = exgtype diff --git a/flopy/mf6/utils/codegen/templates/model.py.jinja b/flopy/mf6/utils/codegen/templates/model.py.jinja index e70c177e22..be981abda3 100644 --- a/flopy/mf6/utils/codegen/templates/model.py.jinja +++ b/flopy/mf6/utils/codegen/templates/model.py.jinja @@ -16,14 +16,22 @@ class Modflow{{ title }}(MFModel): Parameters ---------- + simulation : MFSimulation + Simulation object that this model is a part of. + modelname : str, default "model" + Name of the model. + model_nam_file : str, optional + Relative path to the model name file from model working folder. + version : str, default "mf6" + Version of modflow. + exe_name : str, default "mf6" + Model executable name. + model_rel_path : str or PathLike, default "." (curdir) + Relative path of model folder to simulation folder. {{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + **kwargs + Extra keywords for :class:`flopy.mf6.mfmodel.MFModel`. - Methods - ------- - load : (simulation : MFSimulationData, model_name : string, - namfile : string, version : string, exe_name : string, - model_ws : string, strict : boolean) : MFSimulation - a class method that loads a model from files """ model_type = "{{ title.lower() }}" @@ -35,32 +43,11 @@ class Modflow{{ title }}(MFModel): model_nam_file=None, version="mf6", exe_name="mf6", - model_rel_path=".", + model_rel_path=curdir, {{ macros.init_params(dfn|variables, skip=name|skip_init)|indent(8, first=true) }} **kwargs, ): - """ - {{ description }} - - Parameters - ---------- - modelname : string - name of the model - model_nam_file : string - relative path to the model name file from model working folder - version : string - version of modflow - exe_name : string - model executable name - model_ws : string - model working folder path - sim : MFSimulation - Simulation that this model is a part of. Model is automatically - added to simulation when it is initialized. - -{{ macros.docstrings(dfn|variables)|indent(8, first=true) }} - """ - + """Initialize Modflow{{ title }}.""" super().__init__( simulation, model_type="{{ title.lower() }}6", @@ -89,6 +76,32 @@ class Modflow{{ title }}(MFModel): model_rel_path=curdir, load_only=None, ): + """ + Load an existing {{ title }} model. + + Parameters + ---------- + simulation : MFSimulation + Simulation object that this model is a part of. + structure : MFModelStructure + Structure of this type of model. + modelname : str, default "NewModel" + Name of the model. + model_nam_file : str, default "modflowtest.nam" + Relative path to the model name file from model working folder. + version : str, default "mf6" + Version of modflow. + exe_name : str or PathLike, default "mf6" + Model executable name or path. + strict : bool, default True + Strict mode when loading files. + model_rel_path : str or PathLike, default "." (curdir) + Relative path of model folder to simulation folder. + load_only : list of str, optional + Packages to load (e.g. ['btn', 'adv']). Default None + means that all packages will be loaded. + + """ return MFModel.load_base( cls, simulation, diff --git a/flopy/mf6/utils/codegen/templates/package.py.jinja b/flopy/mf6/utils/codegen/templates/package.py.jinja index b4dc454b78..140a800984 100644 --- a/flopy/mf6/utils/codegen/templates/package.py.jinja +++ b/flopy/mf6/utils/codegen/templates/package.py.jinja @@ -9,7 +9,7 @@ from os import PathLike, curdir from typing import Union from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator -from flopy.mf6.mfpackage import MFPackage, MFChildPackages +from flopy.mf6.mfpackage import MFChildPackages, MFPackage class Modflow{{ title }}(MFPackage): @@ -18,7 +18,20 @@ class Modflow{{ title }}(MFPackage): Parameters ---------- + {{ parent }} + {{ parent|capitalize }} that this package is a part of. Package is automatically + added to {{ parent }} when it is initialized. + loading_package : bool, default False + Do not set this parameter. It is intended for debugging and internal + processing purposes only. {{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + filename : str or PathLike, optional + Name or path of file where this package is stored. + pname : str, optional + Package name. + **kwargs + Extra keywords for :class:`flopy.mf6.mfpackage.MFPackage`. + """ {% for attr in dfn|attrs(name) %} @@ -34,35 +47,14 @@ class Modflow{{ title }}(MFPackage): pname=None, **kwargs, ): - """ - {{ description }} - - Parameters - ---------- - {{ parent }} - {{ parent|capitalize }} that this package is a part of. Package is automatically - added to {{ parent }} when it is initialized. - loading_package : bool - Do not set this parameter. It is intended for debugging and internal - processing purposes only. -{{ macros.docstrings(dfn|variables)|indent(8, first=true) }} - filename : str - File name for this package. - pname : str - Package name for this package. - parent_file : MFPackage - Parent package file that references this package. Only needed for - utility packages (mfutl*). For example, mfutllaktab package must have - a mfgwflak package parent_file. - """ - + """Initialize Modflow{{ title }}.""" super().__init__( - {{ parent }}, - "{{ name[1] }}", - filename, - pname, - loading_package, - **kwargs + parent={{ parent }}, + package_type="{{ name[1] }}", + filename=filename, + pname=pname, + loading_package=loading_package, + **kwargs, ) {% for statement in dfn|init(name) %} @@ -75,19 +67,7 @@ class Modflow{{ title }}(MFPackage): class {{ title }}Packages(MFChildPackages): """ {{ title }}Packages is a container class for the Modflow{{ title }} class. - - Methods - ------- - initialize - Initializes a new Modflow{{ title }} package removing any sibling child - packages attached to the same parent package. See Modflow{{ title }} init - documentation for definition of parameters. - append_package - Adds a new Modflow{{ title }} package to the container. See Modflow{{ title }} - init documentation for definition of parameters. - """ - package_abbr = "{{ title.lower() }}packages" def initialize( @@ -96,6 +76,12 @@ class {{ title }}Packages(MFChildPackages): filename=None, pname=None, ): + """ + Initialize a new Modflow{{ title }} package, removing any sibling + child packages attached to the same parent package. + + See :class:`Modflow{{ title }}` for parameter definitions. + """ new_package = Modflow{{ title }}( self._cpparent, {% for n, var in (dfn|variables).items() if n not in name|skip_init %} @@ -114,6 +100,11 @@ class {{ title }}Packages(MFChildPackages): filename=None, pname=None, ): + """ + Add a new Modflow{{ title }} package to the container. + + See :class:`Modflow{{ title }}` for parameter definitions. + """ new_package = Modflow{{ title }}( self._cpparent, {% for n, var in (dfn|variables).items() if n not in name|skip_init %} diff --git a/flopy/mf6/utils/codegen/templates/simulation.py.jinja b/flopy/mf6/utils/codegen/templates/simulation.py.jinja index d80451232b..61842cd1e4 100644 --- a/flopy/mf6/utils/codegen/templates/simulation.py.jinja +++ b/flopy/mf6/utils/codegen/templates/simulation.py.jinja @@ -15,16 +15,35 @@ class MF{{ title }}(MFSimulationBase): Parameters ---------- + sim_name : str, default "sim" + Name of the simulation. + version : str, default "mf6" + Version of MODFLOW 6 executable. + exe_name : str, default "mf6" + Path to MODFLOW 6 executable. + sim_ws : str or PathLike, default ".' (curdir) + Path to MODFLOW 6 simulation working folder. This is the folder + containing the simulation name file. + verbosity_level : int, default 1 + Verbosity level of standard output: + + 0. No standard output + 1. Standard error/warning messages with some informational + messages + 2. Verbose mode with full error/warning/informational messages. + This is ideal for debugging. + write_headers: bool, default True + When True flopy writes a header to each package file indicating that + it was created by flopy. + lazy_io: bool, default False + When True flopy only reads external data when the data is requested + and only writes external data if the data has changed. This option + automatically overrides the verify_data and auto_set_sizes, turning + both off. + use_pandas: bool, default True + Load/save data using pandas dataframes (for supported data). {{ macros.docstrings(dfn|variables)|indent(4, first=true) }} - Methods - ------- - load : (sim_name : str, version : string, - exe_name : str or PathLike, sim_ws : str or PathLike, strict : bool, - verbosity_level : int, load_only : list, verify_data : bool, - write_headers : bool, lazy_io : bool, use_pandas : bool, - ) : MFSimulation - a class method that loads a simulation from files """ def __init__( @@ -39,30 +58,7 @@ class MF{{ title }}(MFSimulationBase): lazy_io: bool = False, {{ macros.init_params(dfn|variables, skip=name|skip_init)|indent(8, first=true) }} ): - """ - {{ description }} - - Parameters - ---------- - sim_name - The name of the simulation - version - The simulation version - exe_name - The executable name - sim_ws - The simulation workspace - verbosity_level - The verbosity level - write_headers - Whether to write - use_pandas - Whether to use pandas - lazy_io - Whether to use lazy IO -{{ macros.docstrings(dfn|variables)|indent(8, first=true) }} - """ - + """Initialize MF{{ title }}.""" super().__init__( sim_name=sim_name, version=version, @@ -71,7 +67,7 @@ class MF{{ title }}(MFSimulationBase): verbosity_level=verbosity_level, write_headers=write_headers, lazy_io=lazy_io, - use_pandas=use_pandas + use_pandas=use_pandas, ) {% for statement in dfn|init(name) %} @@ -93,17 +89,62 @@ class MF{{ title }}(MFSimulationBase): lazy_io=False, use_pandas=True, ): + """ + Load an existing simulation. + + Parameters + ---------- + sim_name : str, default "modflowsim" + Name of the simulation. + version : str, default "mf6" + Version of MODFLOW 6 executable. + exe_name : str or PathLike, default "mf6" + Path to MODFLOW 6 executable. + sim_ws : str or PathLike, default "." (curdir) + Path to MODFLOW 6 simulation working folder. This is the folder + containing the simulation name file. + strict : bool, default True + Strict enforcement of file formatting. + verbosity_level : int, default 1 + Verbosity level of standard output: + + 0. No standard output + 1. Standard error/warning messages with some informational + messages + 2. Verbose mode with full error/warning/informational messages. + This is ideal for debugging. + load_only : list, optional + List of package abbreviations or package names corresponding to + packages that flopy will load. default is None, which loads all + packages. The discretization packages will load regardless of this + setting. Subpackages, like time series and observations, will also + load regardless of this setting. + Example list: ``['ic', 'maw', 'npf', 'oc', 'ims', 'gwf6-gwf6']`` + verify_data : bool, default False + Verify data when it is loaded. This can slow down loading. + write_headers: bool, default True + When True flopy writes a header to each package file indicating + that it was created by flopy. + lazy_io: bool, default False + When True flopy only reads external data when the data is requested + and only writes external data if the data has changed. This option + automatically overrides the verify_data and auto_set_sizes, turning + both off. + use_pandas: bool, default True + Load/save data using pandas dataframes (for supported data). + + """ return MFSimulationBase.load( cls, - sim_name, - version, - exe_name, - sim_ws, - strict, - verbosity_level, - load_only, - verify_data, - write_headers, - lazy_io, - use_pandas, + sim_name=sim_name, + version=version, + exe_name=exe_name, + sim_ws=sim_ws, + strict=strict, + verbosity_level=verbosity_level, + load_only=load_only, + verify_data=verify_data, + write_headers=write_headers, + lazy_io=lazy_io, + use_pandas=use_pandas, ) diff --git a/flopy/mf6/utils/generate_classes.py b/flopy/mf6/utils/generate_classes.py index b70f17daf9..c6edd9bafc 100644 --- a/flopy/mf6/utils/generate_classes.py +++ b/flopy/mf6/utils/generate_classes.py @@ -91,7 +91,7 @@ def generate_classes( shutil.rmtree(_MF6_AUTOGEN_PATH) _MF6_AUTOGEN_PATH.mkdir(parents=True) - make_all(tomlpath, _MF6_AUTOGEN_PATH, version=2, legacydir=dfnpath) + make_all(tomlpath, _MF6_AUTOGEN_PATH, version=2, legacydir=dfnpath, verbose=verbose) if verbose: files = list(_MF6_AUTOGEN_PATH.glob("*.py")) print(f"Generated {len(files)} module files in: {_MF6_AUTOGEN_PATH}") @@ -132,9 +132,10 @@ def cli_main(): ) parser.add_argument( "--verbose", - action="store_true", + action=argparse.BooleanOptionalAction, default=True, - help="Print information about the code generation process.", + help="Print information about the code generation process; " + "default shows verbose output.", ) parser.add_argument( "--exclude", help="Exclude DFNs matching a pattern.", action="append" From dab7c4a6586aa10c180095fc84d1c9c201400dfe Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Tue, 9 Sep 2025 13:58:28 +1200 Subject: [PATCH 2/2] Remove `assert "items" not in var` for children filter function --- flopy/mf6/utils/codegen/filters.py | 3 +-- flopy/mf6/utils/codegen/templates/model.py.jinja | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/flopy/mf6/utils/codegen/filters.py b/flopy/mf6/utils/codegen/filters.py index fc431cc318..83638dc335 100644 --- a/flopy/mf6/utils/codegen/filters.py +++ b/flopy/mf6/utils/codegen/filters.py @@ -205,8 +205,7 @@ def type(var: dict) -> str: def children(var: dict) -> Optional[dict]: _type = var["type"] - assert "items" not in var, 'TODO: fix bug to use "item" (not "items")' - items = var.get("items", None) # TODO: item = var.get("item", None) + items = var.get("items", None) # TODO: fix to use "item" or "items" fields = var.get("fields", None) choices = var.get("choices", None) if items: diff --git a/flopy/mf6/utils/codegen/templates/model.py.jinja b/flopy/mf6/utils/codegen/templates/model.py.jinja index be981abda3..62ab378a85 100644 --- a/flopy/mf6/utils/codegen/templates/model.py.jinja +++ b/flopy/mf6/utils/codegen/templates/model.py.jinja @@ -77,7 +77,7 @@ class Modflow{{ title }}(MFModel): load_only=None, ): """ - Load an existing {{ title }} model. + Load an existing Modflow{{ title }} model. Parameters ----------