From 681c2074c22b452c6f7346ce1510d6cc14571f0e Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 20 Sep 2024 13:53:15 -0400 Subject: [PATCH 1/3] refactor(createpackages): use jinja for mf6 module code generation --- .docs/md/generate_classes.md | 4 +- .github/workflows/examples.yml | 4 +- .github/workflows/rtd.yml | 2 +- .gitignore | 5 +- docs/mf6_dev_guide.md | 80 +- etc/environment.yml | 8 + flopy/mf6/data/dfn/utl-ts.dfn | 2 + flopy/mf6/data/mfdatastorage.py | 2 +- flopy/mf6/data/mfdatautil.py | 86 -- flopy/mf6/data/mfstructure.py | 437 +------ flopy/mf6/mfbase.py | 17 +- flopy/mf6/mfpackage.py | 2 - flopy/mf6/mfsimbase.py | 11 +- flopy/mf6/utils/codegen/__init__.py | 111 ++ flopy/mf6/utils/codegen/component.py | 57 + flopy/mf6/utils/codegen/filters.py | 560 +++++++++ .../utils/codegen/templates/__init__.py.jinja | 4 + .../utils/codegen/templates/exchange.py.jinja | 85 ++ .../mf6/utils/codegen/templates/macros.jinja | 27 + .../utils/codegen/templates/model.py.jinja | 104 ++ .../utils/codegen/templates/package.py.jinja | 128 ++ .../codegen/templates/simulation.py.jinja | 109 ++ flopy/mf6/utils/createpackages.py | 1053 +---------------- flopy/mf6/utils/generate_classes.py | 18 +- pyproject.toml | 12 +- 25 files changed, 1311 insertions(+), 1617 deletions(-) create mode 100644 flopy/mf6/utils/codegen/__init__.py create mode 100644 flopy/mf6/utils/codegen/component.py create mode 100644 flopy/mf6/utils/codegen/filters.py create mode 100644 flopy/mf6/utils/codegen/templates/__init__.py.jinja create mode 100644 flopy/mf6/utils/codegen/templates/exchange.py.jinja create mode 100644 flopy/mf6/utils/codegen/templates/macros.jinja create mode 100644 flopy/mf6/utils/codegen/templates/model.py.jinja create mode 100644 flopy/mf6/utils/codegen/templates/package.py.jinja create mode 100644 flopy/mf6/utils/codegen/templates/simulation.py.jinja diff --git a/.docs/md/generate_classes.md b/.docs/md/generate_classes.md index f7ac2caf6c..5d234254d2 100644 --- a/.docs/md/generate_classes.md +++ b/.docs/md/generate_classes.md @@ -10,7 +10,9 @@ MODFLOW 6 input continues to evolve as new models, packages, and options are developed, updated, and supported. All MODFLOW 6 input is described by DFN (definition) files, which are simple text files that describe the blocks and keywords in each input file. These definition files are used to build the input and output guide for MODFLOW 6. These definition files are also used to automatically generate FloPy classes for creating, reading and writing MODFLOW 6 models, packages, and options. FloPy and MODFLOW 6 are kept in sync by these DFN (definition) files, and therefore, it may be necessary for a user to update FloPy using a custom set of definition files, or a set of definition files from a previous release. -The FloPy classes for MODFLOW 6 are largely generated by a utility which converts DFN files in a modflow6 repository on GitHub or on the local machine into Python source files in your local FloPy install. For instance (output much abbreviated): +The FloPy classes for MODFLOW 6 are largely generated by a utility which converts DFN files in a modflow6 repository on GitHub or on the local machine into Python source files in your local FloPy install. + +**Note**: to use this functionality, the `codegen` optional dependency group must be installed. ```bash $ python -m flopy.mf6.utils.generate_classes diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 51c581896a..a3f8d8deba 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -34,7 +34,9 @@ jobs: powershell - name: Install FloPy - run: pip install . + run: | + pip install . + pip install ".[codegen]" - name: OpenGL workaround on Linux if: runner.os == 'Linux' diff --git a/.github/workflows/rtd.yml b/.github/workflows/rtd.yml index 29cb251515..6b05849495 100644 --- a/.github/workflows/rtd.yml +++ b/.github/workflows/rtd.yml @@ -17,7 +17,7 @@ concurrency: cancel-in-progress: true jobs: set_options: - name: Set release options + name: Set options runs-on: ubuntu-22.04 outputs: ref: ${{ steps.set_ref.outputs.ref }} diff --git a/.gitignore b/.gitignore index 6d17e3b488..762a0081e6 100644 --- a/.gitignore +++ b/.gitignore @@ -105,5 +105,8 @@ app # DFN backups flopy/mf6/data/dfn_backup/ +# DFN TOML dir +flopy/mf6/data/toml/ + # uv lockfile -uv.lock \ No newline at end of file +uv.lock diff --git a/docs/mf6_dev_guide.md b/docs/mf6_dev_guide.md index 61c364d3de..2d9ac7a365 100644 --- a/docs/mf6_dev_guide.md +++ b/docs/mf6_dev_guide.md @@ -1,47 +1,79 @@ -Introduction ------------------------------------------------ +# Developing FloPy for MF6 -This file provides an overview of how FloPy for MODFLOW 6 (FPMF6) works under the hood and is intended for anyone who wants to add a new package, new model type, or new features to this library. FloPy library files that support MODFLOW 6 can be found in the flopy/mf6 folder and sub-folders. + + -Package Meta-Data and Package Files ------------------------------------------------ +- [Introduction](#introduction) +- [Code generation](#code-generation) +- [Input specification](#input-specification) -FPMF6 uses meta-data files located in flopy/mf6/data/dfn to define the model and package types supported by MODFLOW 6. When additional model and package types are added to MODFLOW 6, additional meta-data files can be added to this folder and flopy/mf6/utils/createpackages.py can be run to add new packages to the FloPy library. createpackages.py uses flopy/mf6/data/mfstructure.py to read meta-data files (*.dfn) and use that meta-data to create the package files found in flopy/mf6/modflow (do not directly modify any of the files in this folder, they are all automatically generated). The automatically generated package files contain an interface for accessing package data and data documentation generated from the meta-data files. Additionally, meta-data describing package data types and shapes is stored in the dfn attribute. flopy/mf6/data/mfstructure.py can load structure information using the dfn attribute (instead of loading it from the meta-data files). This allows for flopy to be installed without the dfn files. + -All meta-data can be accessed from the flopy.mf6.data.mfstructure.MFStructure class. This is a singleton class, meaning only one instance of this class can be created. The class contains a sim_struct attribute (which is a flopy.mf6.data.mfstructure.MFSimulationStructure object) which contains all of the meta-data for all package files. Meta-data is stored in a structured format. MFSimulationStructure contains MFModelStructure and MFInputFileStructure objects, which contain the meta-data for each model type and each "simulation-level" package (tdis, ims, ...). MFModelStructure contains model specific meta-data and a MFInputFileStructure object for each package in that model. MFInputFileStructure contains package specific meta-data and a MFBlockStructure object for each block contained in the package file. MFBlockStructure contains block specific meta-data and a MFDataStructure object for each data structure defined in the block, and MFDataStructure contains data structure specific meta-data and a MFDataItemStructure object for each data item contained in the data structure. Data structures define the structure of data that is naturally grouped together, for example, the data in a numpy recarray. Data item structures define the structure of specific pieces of data, for example, a single column of a numpy recarray. The meta-data defined in these classes provides all the information FloPy needs to read and write MODFLOW 6 package and name files, create the Flopy interface, and check the data for various constraints. +## Introduction +This file provides an overview of how FloPy's MODFLOW 6 module `flopy.mf6` works under the hood. It is intended for FloPy maintainers, as well as anyone who wants to add a new package, new model, or new features to this library. -*** -MFStructure --+ MFSimulationStructure --+ MFModelStructure --+ MFInputFileStructure --+ MFBlockStructure --+ MFDataStructure --+ MFDataItemStructure +## Code generation -Figure 1: FPMF6 generic data structure classes. Lines connecting classes show a relationship defined between the two connected classes. A "*" next to the class means that the class is a sub-class of the connected class. A "+" next to the class means that the class is contained within the connected class. -*** +MODFLOW 6 describes its input specification with definition (DFN) files. -Package and Data Base Classes ------------------------------------------------ +Definition files describe components (e.g. simulations, models, packages) in the MODFLOW 6 input hierarchy. Definition files are used to generate both source code and documentation. -The package and data classes are related as shown below in figure 2. On the top of the figure 2 is the MFPackage class, which is the base class for all packages. MFPackage contains generic methods for building data objects and reading and writing the package to a file. MFPackage contains a MFInputFileStructure object that defines how the data is structured in the package file. MFPackage also contains a dictionary of blocks (MFBlock). The MFBlock class is a generic class used to represent a block within a package. MFBlock contains a MFBlockStructure object that defines how the data in the block is structured. MFBlock also contains a dictionary of data objects (subclasses of MFData) contained in the block and a list of block headers (MFBlockHeader) for that block. Block headers contain the block's name and optionally data items (eg. iprn). +FloPy can generate a MODFLOW 6 compatibility layer for itself, given a set of definition files: + +- `flopy/mf6/utils/createpackages.py`: assumes definition files are in `flopy/mf6/data/dfn` +- `flopy/mf6/utils/generate_classes.py`: downloads DFNs then runs `createpackages.py` + +For instance, to sync with DFNs from the MODFLOW 6 develop branch: +```shell +python -m flopy.mf6.utils.generate_classes --ref develop --no-backup +``` -*** -MFPackage --+ MFBlock --+ MFData +Generated files are created in `flopy/mf6/modflow/`. -MFPackage --+ MFInputFileStructure +The code generation utility downloads DFN files, loads them, and uses Jinja to generate corresponding source files. A definition file typically maps 1-1 to a source file and component class, but 1-many is also possible (e.g. a model definition file yields a model class/file and namefile package class/file). -MFBlock --+ MFBlockStructure +**Note**: Code generation requires a few extra dependencies, grouped in the `codegen` optional dependency group: `Jinja2` and `modflow-devtools`. -MFData --+ MFDataStructure +## Input specification -MFData --* MFArray --* MFTransientArray +The `flopy.mf6.data.mfstructure.MFStructure` class represents an input specification. The class is a singleton, meaning only one instance of this class can be created. The class contains a sim_struct attribute (which is a flopy.mf6.data.mfstructure.MFSimulationStructure object) which contains all of the meta-data for all package files. Meta-data is stored in a structured format. MFSimulationStructure contains MFModelStructure and MFInputFileStructure objects, which contain the meta-data for each model type and each "simulation-level" package (tdis, ims, ...). MFModelStructure contains model specific meta-data and a MFInputFileStructure object for each package in that model. MFInputFileStructure contains package specific meta-data and a MFBlockStructure object for each block contained in the package file. MFBlockStructure contains block specific meta-data and a MFDataStructure object for each data structure defined in the block, and MFDataStructure contains data structure specific meta-data and a MFDataItemStructure object for each data item contained in the data structure. Data structures define the structure of data that is naturally grouped together, for example, the data in a numpy recarray. Data item structures define the structure of specific pieces of data, for example, a single column of a numpy recarray. The meta-data defined in these classes provides all the information FloPy needs to read and write MODFLOW 6 package and name files, create the Flopy interface, and check the data for various constraints. -MFData --* MFList --* MFTransientList +```mermaid +classDiagram + MFStructure *-- "1" MFSimulationStructure : has + MFSimulationStructure *-- "1+" MFModelStructure : has + MFModelStructure *-- "1" MFInputFileStructure : has + MFInputFileStructure *-- "1+" MFBlockStructure : has + MFBlockStructure *-- "1+" MFDataStructure : has + MFDataStructure *-- "1+" MFDataItemStructure : has +``` -MFData --* MFScalar --* MFTransientScalar +Figure 1: Generic data structure hierarchy. Connections show composition relationships. + +The package and data classes are related as shown below in figure 2. On the top of the figure 2 is the MFPackage class, which is the base class for all packages. MFPackage contains generic methods for building data objects and reading and writing the package to a file. MFPackage contains a MFInputFileStructure object that defines how the data is structured in the package file. MFPackage also contains a dictionary of blocks (MFBlock). The MFBlock class is a generic class used to represent a block within a package. MFBlock contains a MFBlockStructure object that defines how the data in the block is structured. MFBlock also contains a dictionary of data objects (subclasses of MFData) contained in the block and a list of block headers (MFBlockHeader) for that block. Block headers contain the block's name and optionally data items (eg. iprn). -MFTransientData --* MFTransientArray, MFTransientList, MFTransientScalar +```mermaid +classDiagram + +MFPackage *-- "1+" MFBlock : has +MFBlock *-- "1+" MFData : has +MFPackage *-- "1" MFInputFileStructure : has +MFBlock *-- "1" MFBlockStructure : has +MFData *-- "1" MFDataStructure : has +MFData <|-- MFArray +MFArray <|-- MFTransientArray +MFData <|-- MFList +MFList <|-- MFTransientList +MFData <|-- MFScalar +MFScalar <|-- MFTransientScalar +MFTransientData <|-- MFTransientArray +MFTransientData <|-- MFTransientList +MFTransientData <|-- MFTransientScalar +``` Figure 2: FPMF6 package and data classes. Lines connecting classes show a relationship defined between the two connected classes. A "*" next to the class means that the class is a sub-class of the connected class. A "+" next to the class means that the class is contained within the connected class. -*** There are three main types of data, MFList, MFArray, and MFScalar data. All three of these data types are derived from the MFData abstract base class. MFList data is the type of data stored in a spreadsheet with different column headings. For example, the data describing a flow barrier are of type MFList. MFList data is stored in numpy recarrays. MFArray data is data of a single type (eg. all integer values). For example, the model's HK values are of type MFArray. MFArrays are stored in numpy ndarrays. MFScalar data is a single data item. Most MFScalar data are options. All MFData subclasses contain an MFDataStructure object that defines the expected structure and types of the data. diff --git a/etc/environment.yml b/etc/environment.yml index 47be84a658..c9e1186992 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -10,6 +10,14 @@ dependencies: - matplotlib>=1.4.0 - pandas>=2.0.0 + # codegen + - boltons>=1.0 + - Jinja2>=3.0 + - tomli + - tomli-w + - pip: + - git+https://github.com/MODFLOW-USGS/modflow-devtools.git + # lint - cffconvert - codespell>=2.2.2 diff --git a/flopy/mf6/data/dfn/utl-ts.dfn b/flopy/mf6/data/dfn/utl-ts.dfn index 41bf114484..9055afd0f7 100644 --- a/flopy/mf6/data/dfn/utl-ts.dfn +++ b/flopy/mf6/data/dfn/utl-ts.dfn @@ -62,6 +62,7 @@ valid stepwise linear linearend shape time_series_names tagged false reader urword +in_record true optional false in_record true longname @@ -112,6 +113,7 @@ name sfacs type keyword shape reader urword +in_record true optional false in_record true longname diff --git a/flopy/mf6/data/mfdatastorage.py b/flopy/mf6/data/mfdatastorage.py index 7f66574c55..0d4db95540 100644 --- a/flopy/mf6/data/mfdatastorage.py +++ b/flopy/mf6/data/mfdatastorage.py @@ -316,7 +316,7 @@ def __init__( self.data_structure_type = data_structure_type package_dim = self.data_dimensions.package_dim self.in_model = ( - self.data_dimensions is not None + package_dim is not None and len(package_dim.package_path) > 1 and package_dim.model_dim[0].model_name is not None and package_dim.model_dim[0].model_name.lower() diff --git a/flopy/mf6/data/mfdatautil.py b/flopy/mf6/data/mfdatautil.py index 45d67913ae..4d20a59b62 100644 --- a/flopy/mf6/data/mfdatautil.py +++ b/flopy/mf6/data/mfdatautil.py @@ -1054,89 +1054,3 @@ def empty( return template else: return rec_array - - -class MFDocString: - """ - Helps build a python class doc string - - Parameters - ---------- - description : string - description of the class - - Attributes - ---------- - indent: string - indent to use in doc string - description : string - description of the class - parameter_header : string - header for parameter section of doc string - parameters : list - list of docstrings for class parameters - - Methods - ------- - add_parameter : (param_descr : string, beginning_of_list : bool) - adds doc string for a parameter with description 'param_descr' to the - end of the list unless beginning_of_list is True - get_doc_string : () : string - builds and returns the docstring for the class - """ - - def __init__(self, description): - self.indent = " " - self.description = description - self.parameter_header = ( - f"{self.indent}Parameters\n{self.indent}----------" - ) - self.parameters = [] - self.model_parameters = [] - - def add_parameter( - self, param_descr, beginning_of_list=False, model_parameter=False - ): - if beginning_of_list: - self.parameters.insert(0, param_descr) - if model_parameter: - self.model_parameters.insert(0, param_descr) - else: - self.parameters.append(param_descr) - if model_parameter: - self.model_parameters.append(param_descr) - - def get_doc_string(self, model_doc_string=False, sim_doc_string=False): - doc_string = '{}"""\n{}{}\n\n{}\n'.format( - self.indent, self.indent, self.description, self.parameter_header - ) - if model_doc_string: - param_list = self.model_parameters - doc_string = ( - "{} modelname : string\n name of the " - "model\n model_nam_file : string\n" - " relative path to the model name file from " - "model working folder\n version : string\n" - " version of modflow\n exe_name : string\n" - " model executable name\n" - " model_ws : string\n" - " model working folder path" - "\n".format(doc_string) - ) - else: - param_list = self.parameters - for parameter in param_list: - if sim_doc_string: - pclean = parameter.strip() - if ( - pclean.startswith("simulation") - or pclean.startswith("loading_package") - or pclean.startswith("filename") - or pclean.startswith("pname") - or pclean.startswith("parent_file") - ): - continue - doc_string += f"{parameter}\n" - if not (model_doc_string or sim_doc_string): - doc_string += f'\n{self.indent}"""' - return doc_string diff --git a/flopy/mf6/data/mfstructure.py b/flopy/mf6/data/mfstructure.py index 34a78a780e..3f321a5101 100644 --- a/flopy/mf6/data/mfstructure.py +++ b/flopy/mf6/data/mfstructure.py @@ -7,13 +7,12 @@ import ast import keyword import os -import warnings from enum import Enum from textwrap import TextWrapper import numpy as np -from ..mfbase import PackageContainer, StructException +from ..mfbase import StructException numeric_index_text = ( "This argument is an index variable, which means that " @@ -421,341 +420,6 @@ def _process_needed_data_items( ] -class DfnFile(Dfn): - """ - Dfn child class that loads dfn information from a package definition (dfn) - file - - Attributes - ---------- - file : str - name of the file to be loaded - - Methods - ------- - dict_by_name : {} : dict - returns a dictionary of data item descriptions from the dfn file with - the data item name as the dictionary key - get_block_structure_dict : (path : tuple, common : bool, model_file : - bool) : dict - returns a dictionary of block structure information for the package - - See Also - -------- - - Notes - ----- - - Examples - -------- - """ - - def __init__(self, file): - super().__init__() - - dfn_path, tail = os.path.split(os.path.realpath(__file__)) - dfn_path = os.path.join(dfn_path, "dfn") - self._file_path = os.path.join(dfn_path, file) - self.dfn_file_name = file - self.dfn_type, self.model_type = self._file_type( - self.dfn_file_name.replace("-", "") - ) - self.package_type = os.path.splitext(file[4:])[0] - # the package type is always the text after the last - - package_name = self.package_type.split("-") - self.package_type = package_name[-1] - if not isinstance(package_name, str) and len(package_name) > 1: - self.package_prefix = "".join(package_name[:-1]) - else: - self.package_prefix = "" - self.file = file - self.dataset_items_needed_dict = {} - self.dfn_list = [] - - def dict_by_name(self): - name_dict = {} - name = None - dfn_fp = open(self._file_path, "r") - for line in dfn_fp: - if self._valid_line(line): - arr_line = line.strip().split() - if arr_line[0] == "name": - name = arr_line[1] - elif arr_line[0] == "description" and name is not None: - name_dict[name] = " ".join(arr_line[1:]) - dfn_fp.close() - return name_dict - - def get_block_structure_dict(self, path, common, model_file, block_parent): - self.dfn_list = [] - block_dict = {} - dataset_items_in_block = {} - self.dataset_items_needed_dict = {} - keystring_items_needed_dict = {} - current_block = None - dfn_fp = open(self._file_path, "r") - - # load header - header_dict = {} - while True: - line = dfn_fp.readline() - if len(line) < 1 or line[0] != "#": - break - line_lst = line.strip().split() - if len(line_lst) > 2 and line_lst[1] == "flopy": - # load flopy data - if line_lst[2] == "multi-package": - header_dict["multi-package"] = True - if line_lst[2] == "parent_name_type" and len(line_lst) == 5: - header_dict["parent_name_type"] = [ - line_lst[3], - line_lst[4], - ] - elif len(line_lst) > 2 and line_lst[1] == "package-type": - header_dict["package-type"] = line_lst[2] - # load file definitions - for line in dfn_fp: - if self._valid_line(line): - # load next data item - new_data_item_struct = MFDataItemStructure() - new_data_item_struct.set_value(line, common) - self.dfn_list.append([line]) - for next_line in dfn_fp: - if self._empty_line(next_line): - break - if self._valid_line(next_line): - new_data_item_struct.set_value(next_line, common) - self.dfn_list[-1].append(next_line) - - # if block does not exist - if ( - current_block is None - or current_block.name != new_data_item_struct.block_name - ): - # create block - current_block = MFBlockStructure( - new_data_item_struct.block_name, - path, - model_file, - block_parent, - ) - # put block in block_dict - block_dict[current_block.name] = current_block - # init dataset item lookup - self.dataset_items_needed_dict = {} - dataset_items_in_block = {} - - # resolve block type - if len(current_block.block_header_structure) > 0: - if ( - len( - current_block.block_header_structure[ - 0 - ].data_item_structures - ) - > 0 - and current_block.block_header_structure[0] - .data_item_structures[0] - .type - == DatumType.integer - ): - block_type = BlockType.transient - else: - block_type = BlockType.multiple - else: - block_type = BlockType.single - - if new_data_item_struct.block_variable: - block_dataset_struct = MFDataStructure( - new_data_item_struct, - model_file, - self.package_type, - self.dfn_list, - ) - block_dataset_struct.parent_block = current_block - self._process_needed_data_items( - block_dataset_struct, dataset_items_in_block - ) - block_dataset_struct.set_path( - path + (new_data_item_struct.block_name,) - ) - block_dataset_struct.add_item( - new_data_item_struct, False, self.dfn_list - ) - current_block.add_dataset(block_dataset_struct) - else: - new_data_item_struct.block_type = block_type - dataset_items_in_block[new_data_item_struct.name] = ( - new_data_item_struct - ) - - # if data item belongs to existing dataset(s) - item_location_found = False - if ( - new_data_item_struct.name - in self.dataset_items_needed_dict - ): - if new_data_item_struct.type == DatumType.record: - # record within a record - create a data set in - # place of the data item - new_data_item_struct = self._new_dataset( - new_data_item_struct, - current_block, - dataset_items_in_block, - path, - model_file, - False, - ) - new_data_item_struct.record_within_record = True - - for dataset in self.dataset_items_needed_dict[ - new_data_item_struct.name - ]: - item_added = dataset.add_item( - new_data_item_struct, True, self.dfn_list - ) - item_location_found = ( - item_location_found or item_added - ) - # if data item belongs to an existing keystring - if ( - new_data_item_struct.name - in keystring_items_needed_dict - ): - new_data_item_struct.set_path( - keystring_items_needed_dict[ - new_data_item_struct.name - ].path - ) - if new_data_item_struct.type == DatumType.record: - # record within a keystring - create a data set in - # place of the data item - new_data_item_struct = self._new_dataset( - new_data_item_struct, - current_block, - dataset_items_in_block, - path, - model_file, - False, - ) - keystring_items_needed_dict[ - new_data_item_struct.name - ].keystring_dict[ - new_data_item_struct.name - ] = new_data_item_struct - item_location_found = True - - if new_data_item_struct.type == DatumType.keystring: - # add keystrings to search list - for ( - key, - val, - ) in new_data_item_struct.keystring_dict.items(): - keystring_items_needed_dict[key] = ( - new_data_item_struct - ) - - # if data set does not exist - if not item_location_found: - self._new_dataset( - new_data_item_struct, - current_block, - dataset_items_in_block, - path, - model_file, - True, - ) - if ( - current_block.name.upper() == "SOLUTIONGROUP" - and len(current_block.block_header_structure) == 0 - ): - # solution_group a special case for now - block_data_item_struct = MFDataItemStructure() - block_data_item_struct.name = "order_num" - block_data_item_struct.data_items = ["order_num"] - block_data_item_struct.type = DatumType.integer - block_data_item_struct.longname = "order_num" - block_data_item_struct.description = ( - "internal variable to keep track of " - "solution group number" - ) - block_dataset_struct = MFDataStructure( - block_data_item_struct, - model_file, - self.package_type, - self.dfn_list, - ) - block_dataset_struct.parent_block = current_block - block_dataset_struct.set_path( - path + (new_data_item_struct.block_name,) - ) - block_dataset_struct.add_item( - block_data_item_struct, False, self.dfn_list - ) - current_block.add_dataset(block_dataset_struct) - dfn_fp.close() - return block_dict, header_dict - - def _new_dataset( - self, - new_data_item_struct, - current_block, - dataset_items_in_block, - path, - model_file, - add_to_block=True, - ): - current_dataset_struct = MFDataStructure( - new_data_item_struct, model_file, self.package_type, self.dfn_list - ) - current_dataset_struct.set_path( - path + (new_data_item_struct.block_name,) - ) - self._process_needed_data_items( - current_dataset_struct, dataset_items_in_block - ) - if add_to_block: - # add dataset - current_block.add_dataset(current_dataset_struct) - current_dataset_struct.parent_block = current_block - current_dataset_struct.add_item( - new_data_item_struct, False, self.dfn_list - ) - return current_dataset_struct - - def _process_needed_data_items( - self, current_dataset_struct, dataset_items_in_block - ): - # add data items needed to dictionary - for ( - item_name, - val, - ) in current_dataset_struct.expected_data_items.items(): - if item_name in dataset_items_in_block: - current_dataset_struct.add_item( - dataset_items_in_block[item_name], False, self.dfn_list - ) - else: - if item_name in self.dataset_items_needed_dict: - self.dataset_items_needed_dict[item_name].append( - current_dataset_struct - ) - else: - self.dataset_items_needed_dict[item_name] = [ - current_dataset_struct - ] - - def _valid_line(self, line): - if len(line.strip()) > 1 and line[0] != "#": - return True - return False - - def _empty_line(self, line): - if len(line.strip()) <= 1: - return True - return False - - class DataType(Enum): """ Types of data that can be found in a package file @@ -1238,7 +902,7 @@ def _resolve_common(arr_line, common): def set_path(self, path): self.path = path + (self.name,) - mfstruct = MFStructure(True) + mfstruct = MFStructure() for dimension in self.shape: dim_path = path + (dimension,) if dim_path in mfstruct.dimension_dict: @@ -2117,7 +1781,6 @@ class MFInputFileStructure: def __init__(self, dfn_file, path, common, model_file): # initialize - self.valid = True self.file_type = dfn_file.package_type self.file_prefix = dfn_file.package_prefix self.dfn_type = dfn_file.dfn_type @@ -2360,7 +2023,7 @@ def process_dfn(self, dfn_file): or dfn_file.dfn_type == DfnType.mvr_file or dfn_file.dfn_type == DfnType.mvt_file ): - model_ver = f"{dfn_file.model_type}{MFStructure(True).get_version_string()}" + model_ver = f"{dfn_file.model_type}{MFStructure().get_version_string()}" if model_ver not in self.model_struct_objs: self.add_model(model_ver) if dfn_file.dfn_type == DfnType.model_file: @@ -2482,104 +2145,44 @@ class MFStructure: _instance = None - def __new__(cls, internal_request=False, load_from_dfn_files=False): + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) # Initialize variables cls._instance.mf_version = 6 - cls._instance.valid = True cls._instance.sim_struct = None cls._instance.dimension_dict = {} - cls._instance.load_from_dfn_files = load_from_dfn_files cls._instance.flopy_dict = {} # Read metadata from file - cls._instance.valid = cls._instance.__load_structure() - elif not cls._instance.valid and not internal_request: - if cls._instance.__load_structure(): - cls._instance.valid = True + cls._instance.valid = cls._instance._load_structure() return cls._instance def get_version_string(self): return format(str(self.mf_version)) - def __load_structure(self): + def _load_structure(self): # set up structure classes self.sim_struct = MFSimulationStructure() # initialize flopy dict keys MFStructure().flopy_dict["solution_packages"] = {} - if self.load_from_dfn_files: - mf_dfn = Dfn() - dfn_files = mf_dfn.get_file_list() - - # get common - common_dfn = DfnFile("common.dfn") - self.sim_struct.process_dfn(common_dfn) - - # process each file's flopy header - for file in dfn_files: - dfn_path, tail = os.path.split(os.path.realpath(__file__)) - dfn_path = os.path.join(dfn_path, "dfn") - dfn_file = os.path.join(dfn_path, file) - with open(dfn_file) as fd_dfn: - for line in fd_dfn: - if len(line) < 1 or line[0] != "#": - break - line_lst = line.strip().split() - if len(line_lst) > 2 and line_lst[1] == "flopy": - # load flopy data - if ( - line_lst[2] == "subpackage" - and len(line_lst) == 7 - ): - sp_dict = { - "construct_package": line_lst[4], - "construct_data": line_lst[5], - "parameter_name": line_lst[6], - } - MFStructure().flopy_dict[line_lst[3]] = sp_dict - elif line_lst[2] == "solution_package": - MFStructure().flopy_dict["solution_packages"][ - line_lst[3] - ] = line_lst[4:] - if len(MFStructure().flopy_dict["solution_packages"]) == 0: - MFStructure().flopy_dict["solution_packages"]["ims"] = ["*"] - warnings.warn( - "Package definition files (dfn) do not define a solution " - "package. This can happen if your dfn files are out of " - "sync. Auto-loaded default IMS solution package metadata." - " In the future auto-loading default metadata will be " - "deprecated.", - DeprecationWarning, - ) - # process each file - for file in dfn_files: - self.sim_struct.process_dfn(DfnFile(file)) - self.sim_struct.tag_read_as_arrays() - else: - package_list = PackageContainer.package_list() - for package in package_list: - # process header - for entry in package.dfn[0][1:]: - if ( - isinstance(entry, list) - and entry[0] == "solution_package" - ): - MFStructure().flopy_dict["solution_packages"][ - package.package_abbr - ] = entry[1:] - # process each package - self.sim_struct.process_dfn(DfnPackage(package)) - self.sim_struct.tag_read_as_arrays() + from ..mfpackage import MFPackage + for package in MFPackage.__subclasses__(): + # process header + for entry in package.dfn[0][1:]: + if ( + isinstance(entry, list) + and entry[0] == "solution_package" + ): + MFStructure().flopy_dict["solution_packages"][ + package.package_abbr + ] = entry[1:] + # process each package + self.sim_struct.process_dfn(DfnPackage(package)) + self.sim_struct.tag_read_as_arrays() return True - - @staticmethod - def __valid_line(line): - if len(line.strip()) > 1 and line[0] != "#": - return True - return False diff --git a/flopy/mf6/mfbase.py b/flopy/mf6/mfbase.py index e2ad3416d9..29eb647358 100644 --- a/flopy/mf6/mfbase.py +++ b/flopy/mf6/mfbase.py @@ -449,7 +449,7 @@ class PackageContainer: """ modflow_packages = [] - packages_by_abbr = {} + packages_by_abbr: dict[str, type] = {} modflow_models = [] models_by_type = {} @@ -460,21 +460,6 @@ def __init__(self, simulation_data): self.package_name_dict = {} self.package_filename_dict = {} - @staticmethod - def package_list(): - """Static method that returns the list of available packages. - For internal FloPy use only, not intended for end users. - - Returns a list of MFPackage subclasses - """ - # all packages except "group" classes - package_list = [] - for abbr, package in sorted(PackageContainer.packages_by_abbr.items()): - # don't store packages "group" classes - if not abbr.endswith("packages"): - package_list.append(package) - return package_list - @staticmethod def package_factory(package_type: str, model_type: str): """Static method that returns the appropriate package type object based diff --git a/flopy/mf6/mfpackage.py b/flopy/mf6/mfpackage.py index c9eb52a881..d3ba2ca00b 100644 --- a/flopy/mf6/mfpackage.py +++ b/flopy/mf6/mfpackage.py @@ -1888,7 +1888,6 @@ def __init__( def __init_subclass__(cls): """Register package type""" super().__init_subclass__() - PackageContainer.modflow_packages.append(cls) PackageContainer.packages_by_abbr[cls.package_abbr] = cls def __setattr__(self, name, value): @@ -3449,7 +3448,6 @@ def __init__( def __init_subclass__(cls): """Register package""" super().__init_subclass__() - PackageContainer.modflow_packages.append(cls) PackageContainer.packages_by_abbr[cls.package_abbr] = cls def __getattr__(self, attr): diff --git a/flopy/mf6/mfsimbase.py b/flopy/mf6/mfsimbase.py index 74dc8d036a..9546219ad0 100644 --- a/flopy/mf6/mfsimbase.py +++ b/flopy/mf6/mfsimbase.py @@ -483,15 +483,6 @@ def __init__( self.simulation_data.lazy_io = True self._package_container = PackageContainer(self.simulation_data) - # verify metadata - fpdata = mfstructure.MFStructure() - if not fpdata.valid: - excpt_str = ( - "Invalid package metadata. Unable to load MODFLOW " - "file structure metadata." - ) - raise FlopyException(excpt_str) - # initialize self.dimensions = None self.type = "Simulation" @@ -503,7 +494,7 @@ def __init__( self._exchange_files = {} self._solution_files = {} self._other_files = {} - self.structure = fpdata.sim_struct + self.structure = mfstructure.MFStructure().sim_struct self.model_type = None self._exg_file_num = {} diff --git a/flopy/mf6/utils/codegen/__init__.py b/flopy/mf6/utils/codegen/__init__.py new file mode 100644 index 0000000000..da769ba065 --- /dev/null +++ b/flopy/mf6/utils/codegen/__init__.py @@ -0,0 +1,111 @@ +from itertools import chain +from os import PathLike +from pathlib import Path + +__all__ = ["make_init", "make_targets", "make_all"] + + +def _get_template_env(): + # import here instead of module so we don't + # expect optional deps at module init time + import jinja2 + + loader = jinja2.PackageLoader("flopy", "mf6/utils/codegen/templates/") + env = jinja2.Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True, + 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 + + return env + + +def make_init(dfns: dict, outdir: PathLike, verbose: bool = False): + """Generate a Python __init__.py file for the given input definitions.""" + + env = _get_template_env() + outdir = Path(outdir).expanduser().absolute() + + # import here instead of module so we don't + # expect optional deps at module init time + from flopy.mf6.utils.codegen.component import ComponentDescriptor + + components = list( + chain.from_iterable(ComponentDescriptor.from_dfn(dfn) for dfn in dfns.values()) + ) + target_name = "__init__.py" + target_path = outdir / target_name + template = env.get_template(f"{target_name}.jinja") + with open(target_path, "w") as f: + f.write(template.render(components=components)) + if verbose: + print(f"Wrote {target_path}") + + +def make_targets(dfn, outdir: PathLike, verbose: bool = False): + """Generate Python source file(s) from the given input definition.""" + + env = _get_template_env() + outdir = Path(outdir).expanduser().absolute() + + # import here instead of module so we don't + # expect optional deps at module init time + 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) + if base == "MFSimulationBase": + return "simulation.py.jinja" + elif base == "MFModel": + return "model.py.jinja" + elif base == "MFPackage": + if component_name[0] == "exg": + return "exchange.py.jinja" + return "package.py.jinja" + else: + raise NotImplementedError(f"Unknown base class: {base}") + + for component in ComponentDescriptor.from_dfn(dfn): + component_name = component["name"] + 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)) + if verbose: + print(f"Wrote {target_path}") + + +def make_all(dfndir: Path, outdir: PathLike, verbose: bool = False, version: int = 1): + """Generate Python source files from the DFN files in the given location.""" + + # import here instead of module so we don't + # expect optional deps at module init time + from modflow_devtools.dfn import Dfn + + dfns = Dfn.load_all(dfndir, version=version) + make_init(dfns, outdir, verbose) + for dfn in dfns.values(): + make_targets(dfn, outdir, verbose) diff --git a/flopy/mf6/utils/codegen/component.py b/flopy/mf6/utils/codegen/component.py new file mode 100644 index 0000000000..76845819b7 --- /dev/null +++ b/flopy/mf6/utils/codegen/component.py @@ -0,0 +1,57 @@ +from typing import ( + Iterator, + TypedDict, +) + +from modflow_devtools.dfn import Dfn + + +def get_component_names(dfn: dict) -> list[tuple[str, str]]: + """ + Get the names of components produced by the definition. + A definition may produce one or more component classes. + """ + name = dfn.get("name", None) + if not name: + raise ValueError(f"DFN must have a 'name' entry") + name = name.split("-") + if name[1] == "nam": + if name[0] == "sim": + return [ + (None, name[1]), # nam pkg + tuple([*name]), # simulation + ] + else: + return [ + tuple([*name]), # nam pkg + (name[0], None), # model + ] + elif name in [ + ["gwf", "mvr"], + ["gwf", "gnc"], + ["gwt", "mvt"], + ]: + # TODO: deduplicate mfmvr.py/mfgwfmvr.py etc and remove special cases + return [ + tuple([*name]), + (None, name[1]), + ] + return [tuple([*name])] + + +class ComponentDescriptor(TypedDict): + """ + MODFLOW 6 input component class descriptor. Each component is + specified by a definition file. A definition file specifies 1+ + components (e.g. model DFNs yield a model and a package class). + """ + + name: tuple[str, str] + + @staticmethod + def from_dfn(dfn: Dfn) -> Iterator["ComponentDescriptor"]: + """ + Yield component class descriptors from an input definition. + """ + for name in get_component_names(dfn): + yield ComponentDescriptor(name=name, dfn=dfn) diff --git a/flopy/mf6/utils/codegen/filters.py b/flopy/mf6/utils/codegen/filters.py new file mode 100644 index 0000000000..858dad19e2 --- /dev/null +++ b/flopy/mf6/utils/codegen/filters.py @@ -0,0 +1,560 @@ +import sys +from enum import Enum +from keyword import kwlist +from pathlib import Path +from pprint import pformat +from typing import Any, List, Optional + +from boltons.iterutils import default_enter, remap + + +def _try_get_enum_value(v: Any) -> Any: + """ + Get the enum's value if the object is an instance + of an enumeration, otherwise return it unaltered. + """ + return v.value if isinstance(v, Enum) else v + + +def _get_vars(d: dict) -> dict[str, dict]: + vars_ = dict() + def visit(p, k, v): + if isinstance(v, dict) and "type" in v: + vars_[k] = v + return True + def enter(p, k, v): + if isinstance(v, dict) and "type" in v: + return (v, False) + return default_enter(p, k, v) + + remap(d, enter=enter, visit=visit) + 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 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 + 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 == "list": + if len(children) == 1: + first = list(children.values())[0] + if first["type"] in ["record", "union"]: + 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 == "union": + 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 == "list" + return items + if fields: + assert _type == "record" + return fields + if choices: + assert _type == "union" + 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 _MF6_SCALARS, Dfn + + 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 _MF6_SCALARS 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) + 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 ["list", "record", "union"] + 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 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_dir = Path(__file__).parents[2] / "data" / "dfn" + + 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 [ + " ".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_ + + with open(dfn_dir / "common.dfn") as common_f, \ + open(dfn_dir / dfn_file_name) as f: + common, _ = Dfn._load_v1_flat(common_f) + legacy_dfn, metadata = Dfn._load_v1_flat(f, common=common) + legacy_dfn = _dfn(legacy_dfn, _filter_metadata(metadata)) + 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 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": + + 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.{'_' 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 + 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]) + ] + 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/__init__.py.jinja b/flopy/mf6/utils/codegen/templates/__init__.py.jinja new file mode 100644 index 0000000000..c5ec021e7f --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/__init__.py.jinja @@ -0,0 +1,4 @@ +# autogenerated file, do not modify +{% for c in components %} +from .mf{{ c.name|title }} import {{ c.name|prefix }}{{ (c.name|title).title() }} +{% endfor %} \ No newline at end of file diff --git a/flopy/mf6/utils/codegen/templates/exchange.py.jinja b/flopy/mf6/utils/codegen/templates/exchange.py.jinja new file mode 100644 index 0000000000..9d3604dcb5 --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/exchange.py.jinja @@ -0,0 +1,85 @@ +# autogenerated file, do not modify +{% import 'macros.jinja' as macros %} +{% set parent = dfn|parent(name) %} +{% set title = (name|title).title() %} +{% set description = name|description %} + + +from os import PathLike, curdir +from typing import Union + +from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator +from flopy.mf6.mfpackage import MFPackage + +class Modflow{{ title }}(MFPackage): + """ + {{ description }} + + Parameters + ---------- +{{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + """ + + {% for attr in dfn|attrs(name) %} + {{ attr }} + {% endfor %} + + def __init__( + self, + simulation, + loading_package=False, + exgtype="{{ name[1][:3].upper() }}6-{{ name[1][3:].upper() }}6", + exgmnamea=None, + exgmnameb=None, +{{ macros.init_params(dfn|variables, skip=name|skip_init)|indent(8, first=true) }} + filename=None, + 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) }} + """ + + super().__init__( + {{ parent }}, + "{{ name[1] }}", + filename, + pname, + loading_package, + **kwargs + ) + + self.exgtype = exgtype + self.exgmnamea = exgmnamea + self.exgmnameb = exgmnameb + simulation.register_exchange_file(self) + + {% for statement in dfn|init(name) %} + {{ statement }} + {% endfor %} + + self._init_complete = True \ No newline at end of file diff --git a/flopy/mf6/utils/codegen/templates/macros.jinja b/flopy/mf6/utils/codegen/templates/macros.jinja new file mode 100644 index 0000000000..0d5be0a225 --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/macros.jinja @@ -0,0 +1,27 @@ +{% macro init_params(vars, alias=false, skip=none) %} +{% for name, var in vars.items() if name not in skip %} +{% set v = var|untag %} +{% set n = (name if alias else v.name)|safe_name %} +{{ n }}={{ v|default_value|value }}, +{% endfor %} +{% endmacro %} + +{% macro docstrings(vars, recurse=true) %} +{% for var in vars.values() recursive %} +{% set v = var|untag %} +{% set n = v.name|safe_name %} +{% set children = v|children %} +{% if loop.depth > 1 %}* {% endif %}{{ n }} : {{ v|type }} +{% if v.description is defined and v.description is not none %} +{{ v.description|clean|math|wordwrap|indent(loop.depth * 4, first=true) }} +{% endif %} +{% if recurse and children is not none %} +{% if v.type == "list" and children|length == 1 and (children.values()|first).type in ["record", "union"] %} +{% set grandchildren = (children.values()|first)|children %} +{{ loop(grandchildren.values())|indent(loop.depth * 4, first=true) }} +{% else %} +{{ loop(children.values())|indent(loop.depth * 4, first=true) }} +{% endif %} +{% endif %} +{% endfor %} +{% endmacro %} diff --git a/flopy/mf6/utils/codegen/templates/model.py.jinja b/flopy/mf6/utils/codegen/templates/model.py.jinja new file mode 100644 index 0000000000..e70c177e22 --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/model.py.jinja @@ -0,0 +1,104 @@ +# autogenerated file, do not modify +{% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} +{% set description = name|description %} + +from os import PathLike, curdir +from typing import Union + +from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator +from flopy.mf6.mfmodel import MFModel + + +class Modflow{{ title }}(MFModel): + """ + {{ description }} + + Parameters + ---------- +{{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + + 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() }}" + + def __init__( + self, + simulation, + modelname="model", + model_nam_file=None, + version="mf6", + exe_name="mf6", + model_rel_path=".", +{{ 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) }} + """ + + super().__init__( + simulation, + model_type="{{ title.lower() }}6", + modelname=modelname, + model_nam_file=model_nam_file, + version=version, + exe_name=exe_name, + model_rel_path=model_rel_path, + **kwargs, + ) + + {% for statement in dfn|init(name) %} + {{ statement }} + {% endfor %} + + @classmethod + def load( + cls, + simulation, + structure, + modelname="NewModel", + model_nam_file="modflowtest.nam", + version="mf6", + exe_name="mf6", + strict=True, + model_rel_path=curdir, + load_only=None, + ): + return MFModel.load_base( + cls, + simulation, + structure, + modelname, + model_nam_file, + "{{ title.lower() }}6", + version, + exe_name, + strict, + model_rel_path, + load_only, + ) diff --git a/flopy/mf6/utils/codegen/templates/package.py.jinja b/flopy/mf6/utils/codegen/templates/package.py.jinja new file mode 100644 index 0000000000..b4dc454b78 --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/package.py.jinja @@ -0,0 +1,128 @@ +# autogenerated file, do not modify +{% import 'macros.jinja' as macros %} +{% set parent = dfn|parent(name) %} +{% set title = (name|title).title() %} +{% set description = name|description %} +{% set container = dfn.ref is defined and name[0] not in ["sln", "sim"] and name[1] not in ["hpc"] %} + +from os import PathLike, curdir +from typing import Union + +from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator +from flopy.mf6.mfpackage import MFPackage, MFChildPackages + + +class Modflow{{ title }}(MFPackage): + """ + {{ description }} + + Parameters + ---------- +{{ macros.docstrings(dfn|variables)|indent(4, first=true) }} + """ + + {% for attr in dfn|attrs(name) %} + {{ attr }} + {% endfor %} + + def __init__( + self, + {{ parent }}, + loading_package=False, +{{ macros.init_params(dfn|variables, skip=name|skip_init)|indent(8, first=true) }} + filename=None, + 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. + """ + + super().__init__( + {{ parent }}, + "{{ name[1] }}", + filename, + pname, + loading_package, + **kwargs + ) + + {% for statement in dfn|init(name) %} + {{ statement }} + {% endfor %} + + self._init_complete = True + +{% if container %} +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( + self, +{{ macros.init_params(dfn|variables, alias=true, skip=name|skip_init)|indent(8, first=true) }} + filename=None, + pname=None, + ): + new_package = Modflow{{ title }}( + self._cpparent, + {% for n, var in (dfn|variables).items() if n not in name|skip_init %} + {{ n|safe_name }}={{ n|safe_name }}, + {% endfor %} + filename=filename, + pname=pname, + child_builder_call=True, + ) + self.init_package(new_package, filename) + + {% if name[1] != "obs" %} + def append_package( + self, +{{ macros.init_params(dfn|variables, alias=true, skip=name|skip_init)|indent(8, first=true) }} + filename=None, + pname=None, + ): + new_package = Modflow{{ title }}( + self._cpparent, + {% for n, var in (dfn|variables).items() if n not in name|skip_init %} + {{ n|safe_name }}={{ n|safe_name }}, + {% endfor %} + filename=filename, + pname=pname, + child_builder_call=True, + ) + self._append_package(new_package, filename) + {% endif %} +{% endif %} diff --git a/flopy/mf6/utils/codegen/templates/simulation.py.jinja b/flopy/mf6/utils/codegen/templates/simulation.py.jinja new file mode 100644 index 0000000000..d80451232b --- /dev/null +++ b/flopy/mf6/utils/codegen/templates/simulation.py.jinja @@ -0,0 +1,109 @@ +# autogenerated file, do not modify +{% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} +{% set description = name|description %} + +from os import PathLike, curdir +from typing import Union + +from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator +from flopy.mf6.mfsimbase import MFSimulationBase + +class MF{{ title }}(MFSimulationBase): + """ + {{ description }} + + Parameters + ---------- +{{ 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__( + self, + sim_name: str = "sim", + version: str = "mf6", + exe_name: Union[str, PathLike] = "mf6", + sim_ws: Union[str, PathLike] = curdir, + verbosity_level: int = 1, + write_headers: bool = True, + use_pandas: bool = True, + 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) }} + """ + + super().__init__( + sim_name=sim_name, + version=version, + exe_name=exe_name, + sim_ws=sim_ws, + verbosity_level=verbosity_level, + write_headers=write_headers, + lazy_io=lazy_io, + use_pandas=use_pandas + ) + + {% for statement in dfn|init(name) %} + {{ statement }} + {% endfor %} + + @classmethod + def load( + cls, + sim_name="modflowsim", + version="mf6", + exe_name: Union[str, PathLike] = "mf6", + sim_ws: Union[str, PathLike] = curdir, + strict=True, + verbosity_level=1, + load_only=None, + verify_data=False, + write_headers=True, + lazy_io=False, + use_pandas=True, + ): + return MFSimulationBase.load( + cls, + sim_name, + version, + exe_name, + sim_ws, + strict, + verbosity_level, + load_only, + verify_data, + write_headers, + lazy_io, + use_pandas, + ) diff --git a/flopy/mf6/utils/createpackages.py b/flopy/mf6/utils/createpackages.py index e1a57fb094..75ef5ed356 100644 --- a/flopy/mf6/utils/createpackages.py +++ b/flopy/mf6/utils/createpackages.py @@ -81,1056 +81,11 @@ """ -import datetime -import os -import textwrap -from enum import Enum +from pathlib import Path -# keep below as absolute imports -from flopy.mf6.data import mfdatautil, mfstructure -from flopy.utils import datautil - - -class PackageLevel(Enum): - sim_level = 0 - model_level = 1 - - -def build_doc_string(param_name, param_type, param_desc, indent): - return f"{indent}{param_name} : {param_type}\n{indent * 2}* {param_desc}" - - -def generator_type(data_type): - if ( - data_type == mfstructure.DataType.scalar_keyword - or data_type == mfstructure.DataType.scalar - ): - # regular scalar - return "ScalarTemplateGenerator" - elif ( - data_type == mfstructure.DataType.scalar_keyword_transient - or data_type == mfstructure.DataType.scalar_transient - ): - # transient scalar - return "ScalarTemplateGenerator" - elif data_type == mfstructure.DataType.array: - # array - return "ArrayTemplateGenerator" - elif data_type == mfstructure.DataType.array_transient: - # transient array - return "ArrayTemplateGenerator" - elif data_type == mfstructure.DataType.list: - # list - return "ListTemplateGenerator" - elif ( - data_type == mfstructure.DataType.list_transient - or data_type == mfstructure.DataType.list_multiple - ): - # transient or multiple list - return "ListTemplateGenerator" - - -def clean_class_string(name): - if len(name) > 0: - clean_string = name.replace(" ", "_") - clean_string = clean_string.replace("-", "_") - version = mfstructure.MFStructure().get_version_string() - # FIX: remove all numbers - if clean_string[-1] == version: - clean_string = clean_string[:-1] - return clean_string - return name - - -def build_dfn_string(dfn_list, header, package_abbr, flopy_dict): - dfn_string = " dfn = [" - line_length = len(dfn_string) - leading_spaces = " " * line_length - first_di = True - - # process header - dfn_string = f'{dfn_string}\n{leading_spaces}["header", ' - for key, value in header.items(): - if key == "multi-package": - dfn_string = f'{dfn_string}\n{leading_spaces} "multi-package", ' - if key == "package-type": - dfn_string = ( - f'{dfn_string}\n{leading_spaces} "package-type ' f'{value}"' - ) - - # process solution packages - if package_abbr in flopy_dict["solution_packages"]: - model_types = '", "'.join( - flopy_dict["solution_packages"][package_abbr] - ) - dfn_string = ( - f"{dfn_string}\n{leading_spaces} " - f'["solution_package", "{model_types}"], ' - ) - dfn_string = f"{dfn_string}],\n{leading_spaces}" - - # process all data items - for data_item in dfn_list: - line_length += 1 - if not first_di: - dfn_string = f"{dfn_string},\n{leading_spaces}" - line_length = len(leading_spaces) - else: - first_di = False - dfn_string = f"{dfn_string}[" - first_line = True - # process each line in a data item - for line in data_item: - line = line.strip() - # do not include the description of longname - if not line.lower().startswith( - "description" - ) and not line.lower().startswith("longname"): - line = line.replace('"', "'") - line_length += len(line) + 4 - if not first_line: - dfn_string = f"{dfn_string}," - if line_length < 77: - # added text fits on the current line - if first_line: - dfn_string = f'{dfn_string}"{line}"' - else: - dfn_string = f'{dfn_string} "{line}"' - else: - # added text does not fit on the current line - line_length = len(line) + len(leading_spaces) + 2 - if line_length > 79: - # added text too long to fit on a single line, wrap - # text as needed - line = f'"{line}"' - lines = textwrap.wrap( - line, - 75 - len(leading_spaces), - drop_whitespace=True, - ) - lines[0] = f"{leading_spaces} {lines[0]}" - line_join = f' "\n{leading_spaces} "' - dfn_string = f"{dfn_string}\n{line_join.join(lines)}" - else: - dfn_string = f'{dfn_string}\n{leading_spaces} "{line}"' - first_line = False - - dfn_string = f"{dfn_string}]" - dfn_string = f"{dfn_string}]" - return dfn_string - - -def create_init_var(clean_ds_name, data_structure_name, init_val=None): - if init_val is None: - init_val = clean_ds_name - - init_var = f" self.{clean_ds_name} = self.build_mfdata(" - leading_spaces = " " * len(init_var) - if len(init_var) + len(data_structure_name) + 2 > 79: - second_line = f'\n "{data_structure_name}",' - if len(second_line) + len(clean_ds_name) + 2 > 79: - init_var = f"{init_var}{second_line}\n {init_val})" - else: - init_var = f"{init_var}{second_line} {init_val})" - else: - init_var = f'{init_var}"{data_structure_name}",' - if len(init_var) + len(clean_ds_name) + 2 > 79: - init_var = f"{init_var}\n{leading_spaces}{init_val})" - else: - init_var = f"{init_var} {init_val})" - return init_var - - -def create_basic_init(clean_ds_name): - return f" self.{clean_ds_name} = {clean_ds_name}\n" - - -def create_property(clean_ds_name): - return f" {clean_ds_name} = property(get_{clean_ds_name}, set_{clean_ds_name})" - - -def format_var_list(base_string, var_list, is_tuple=False): - if is_tuple: - base_string = f"{base_string}(" - extra_chars = 4 - else: - extra_chars = 2 - line_length = len(base_string) - leading_spaces = " " * line_length - # determine if any variable name is too long to fit - for item in var_list: - if line_length + len(item) + extra_chars > 80: - leading_spaces = " " - base_string = f"{base_string}\n{leading_spaces}" - line_length = len(leading_spaces) - break - - for index, item in enumerate(var_list): - if is_tuple: - item = f"'{item}'" - if index == len(var_list) - 1: - next_var_str = item - else: - next_var_str = f"{item}," - line_length += len(item) + extra_chars - if line_length > 80: - base_string = f"{base_string}\n{leading_spaces}{next_var_str}" - else: - if base_string[-1] == ",": - base_string = f"{base_string} " - base_string = f"{base_string}{next_var_str}" - if is_tuple: - return f"{base_string}))" - else: - return f"{base_string})" - - -def create_package_init_var( - parameter_name, package_abbr, data_name, clean_ds_name -): - one_line = ( - f" self._{package_abbr}_package = self.build_child_package(" - ) - one_line_b = f'"{package_abbr}", {parameter_name},' - leading_spaces = " " * len(one_line) - two_line = f'\n{leading_spaces}"{data_name}",' - three_line = f"\n{leading_spaces}self._{clean_ds_name})" - return f"{one_line}{one_line_b}{two_line}{three_line}" - - -def add_var( - init_vars, - class_vars, - options_param_list, - init_param_list, - package_properties, - doc_string, - data_structure_dict, - default_value, - name, - python_name, - description, - path, - data_type, - basic_init=False, - construct_package=None, - construct_data=None, - parameter_name=None, - set_param_list=None, - mf_nam=False, -): - if set_param_list is None: - set_param_list = [] - clean_ds_name = datautil.clean_name(python_name) - if construct_package is None: - # add variable initialization lines - if basic_init: - init_vars.append(create_basic_init(clean_ds_name)) - else: - init_vars.append(create_init_var(clean_ds_name, name)) - # add to parameter list - if default_value is None: - default_value = "None" - init_param_list.append(f"{clean_ds_name}={default_value}") - if path is not None and "options" in path: - options_param_list.append(f"{clean_ds_name}={default_value}") - # add to set parameter list - set_param_list.append(f"{clean_ds_name}={clean_ds_name}") - else: - clean_parameter_name = datautil.clean_name(parameter_name) - # init hidden variable - init_vars.append(create_init_var(f"_{clean_ds_name}", name, "None")) - if mf_nam: - options_param_list.append( - [f"{parameter_name}_data=None", parameter_name] - ) - else: - # init child package - init_vars.append( - create_package_init_var( - clean_parameter_name, - construct_package, - construct_data, - clean_ds_name, - ) - ) - # add to parameter list - init_param_list.append(f"{clean_parameter_name}=None") - # add to set parameter list - set_param_list.append( - f"{clean_parameter_name}={clean_parameter_name}" - ) - - package_properties.append(create_property(clean_ds_name)) - doc_string.add_parameter(description, model_parameter=True) - data_structure_dict[python_name] = 0 - if class_vars is not None: - gen_type = generator_type(data_type) - if gen_type != "ScalarTemplateGenerator": - new_class_var = f" {clean_ds_name} = {gen_type}(" - class_vars.append(format_var_list(new_class_var, path, True)) - return gen_type - return None - - -def build_init_string( - init_string, init_param_list, whitespace=" " -): - line_chars = len(init_string) - for index, param in enumerate(init_param_list): - if isinstance(param, list): - param = param[0] - if index + 1 < len(init_param_list): - line_chars += len(param) + 2 - else: - line_chars += len(param) + 3 - if line_chars > 79: - if len(param) + len(whitespace) + 1 > 79: - # try to break apart at = sign - param_list = param.split("=") - if len(param_list) == 2: - init_string = "{},\n{}{}=\n{}{}".format( - init_string, - whitespace, - param_list[0], - whitespace, - param_list[1], - ) - line_chars = len(param_list[1]) + len(whitespace) + 1 - continue - init_string = f"{init_string},\n{whitespace}{param}" - line_chars = len(param) + len(whitespace) + 1 - else: - init_string = f"{init_string}, {param}" - return f"{init_string}):\n" - - -def build_model_load(model_type): - model_load_c = ( - " Methods\n -------\n" - " load : (simulation : MFSimulationData, model_name : " - "string,\n namfile : string, " - "version : string, exe_name : string,\n model_ws : " - "string, strict : boolean) : MFSimulation\n" - " a class method that loads a model from files" - '\n """' - ) - - model_load = ( - " @classmethod\n def load(cls, simulation, structure, " - "modelname='NewModel',\n " - "model_nam_file='modflowtest.nam', version='mf6',\n" - " exe_name='mf6', strict=True, " - "model_rel_path='.',\n" - " load_only=None):\n " - "return mfmodel.MFModel.load_base(cls, simulation, structure, " - "modelname,\n " - "model_nam_file, '{}6', version,\n" - " exe_name, strict, " - "model_rel_path,\n" - " load_only)" - "\n".format(model_type) - ) - return model_load, model_load_c - - -def build_sim_load(): - sim_load_c = ( - " Methods\n -------\n" - " load : (sim_name : str, version : " - "string,\n exe_name : str or PathLike, " - "sim_ws : str or PathLike, strict : bool,\n verbosity_level : " - "int, load_only : list, verify_data : bool,\n " - "write_headers : bool, lazy_io : bool, use_pandas : bool,\n " - ") : MFSimulation\n" - " a class method that loads a simulation from files" - '\n """' - ) - - sim_load = ( - " @classmethod\n def load(cls, sim_name='modflowsim', " - "version='mf6',\n " - "exe_name: Union[str, os.PathLike] = 'mf6',\n " - "sim_ws: Union[str, os.PathLike] = os.curdir,\n " - "strict=True, verbosity_level=1, load_only=None,\n " - "verify_data=False, write_headers=True,\n " - "lazy_io=False, use_pandas=True):\n " - "return mfsimbase.MFSimulationBase.load(cls, sim_name, version, " - "\n " - "exe_name, sim_ws, strict,\n" - " verbosity_level, " - "load_only,\n " - "verify_data, write_headers, " - "\n lazy_io, use_pandas)" - "\n" - ) - return sim_load, sim_load_c - - -def build_model_init_vars(param_list): - init_var_list = [] - # build set data calls - for param in param_list: - if not isinstance(param, list): - param_parts = param.split("=") - init_var_list.append( - f" self.name_file.{param_parts[0]}.set_data({param_parts[0]})" - ) - init_var_list.append("") - # build attributes - for param in param_list: - if isinstance(param, list): - pkg_name = param[1] - param_parts = param[0].split("=") - init_var_list.append( - f" self.{param_parts[0]} = " - f"self._create_package('{pkg_name}', {param_parts[0]})" - ) - else: - param_parts = param.split("=") - init_var_list.append( - f" self.{param_parts[0]} = self.name_file.{param_parts[0]}" - ) - - return "\n".join(init_var_list) - - -def create_packages(): - indent = " " - init_string_def = " def __init__(self" - - # load JSON file - file_structure = mfstructure.MFStructure(load_from_dfn_files=True) - sim_struct = file_structure.sim_struct - - # assemble package list of buildable packages - package_list = [] - for package in sim_struct.utl_struct_objs.values(): - # add utility packages to list - package_list.append( - ( - package, - PackageLevel.model_level, - "utl", - package.dfn_list, - package.file_type, - package.header, - ) - ) - package_list.append( - ( - sim_struct.name_file_struct_obj, - PackageLevel.sim_level, - "", - sim_struct.name_file_struct_obj.dfn_list, - sim_struct.name_file_struct_obj.file_type, - sim_struct.name_file_struct_obj.header, - ) - ) - for package in sim_struct.package_struct_objs.values(): - # add simulation level package to list - package_list.append( - ( - package, - PackageLevel.sim_level, - "", - package.dfn_list, - package.file_type, - package.header, - ) - ) - for model_key, model in sim_struct.model_struct_objs.items(): - package_list.append( - ( - model.name_file_struct_obj, - PackageLevel.model_level, - model_key, - model.name_file_struct_obj.dfn_list, - model.name_file_struct_obj.file_type, - model.name_file_struct_obj.header, - ) - ) - for package in model.package_struct_objs.values(): - package_list.append( - ( - package, - PackageLevel.model_level, - model_key, - package.dfn_list, - package.file_type, - package.header, - ) - ) - - util_path, tail = os.path.split(os.path.realpath(__file__)) - init_file = open( - os.path.join(util_path, "..", "modflow", "__init__.py"), - "w", - newline="\n", - ) - init_file.write("from .mfsimulation import MFSimulation # isort:skip\n") - - nam_import_string = ( - "from .. import mfmodel\nfrom ..data.mfdatautil " - "import ArrayTemplateGenerator, ListTemplateGenerator" - ) - - # loop through packages list - init_file_imports = [] - flopy_dict = file_structure.flopy_dict - for package in package_list: - data_structure_dict = {} - package_properties = [] - init_vars = [] - init_param_list = [] - options_param_list = [] - set_param_list = [] - class_vars = [] - template_gens = [] - - package_abbr = clean_class_string( - f"{clean_class_string(package[2])}{package[0].file_type}" - ).lower() - dfn_string = build_dfn_string( - package[3], package[5], package_abbr, flopy_dict - ) - package_name = clean_class_string( - "{}{}{}".format( - clean_class_string(package[2]), - package[0].file_prefix, - package[0].file_type, - ) - ).lower() - if package[0].description: - doc_string = mfdatautil.MFDocString(package[0].description) - else: - if package[2]: - package_container_text = f" within a {package[2]} model" - else: - package_container_text = "" - ds = "Modflow{} defines a {} package{}.".format( - package_name.title(), - package[0].file_type, - package_container_text, - ) - if package[0].file_type == "mvr": - # mvr package warning - if package[2]: - ds = ( - "{} This package\n can only be used to move " - "water between packages within a single model." - "\n To move water between models use ModflowMvr" - ".".format(ds) - ) - else: - ds = ( - "{} This package can only be used to move\n " - "water between two different models. To move " - "water between two packages\n in the same " - 'model use the "model level" mover package (ex. ' - "ModflowGwfmvr).".format(ds) - ) - - doc_string = mfdatautil.MFDocString(ds) - - if package[0].dfn_type == mfstructure.DfnType.exch_file: - exgtype = ( - f'"{package_abbr[0:3].upper()}6-{package_abbr[3:].upper()}6"' - ) - - add_var( - init_vars, - None, - options_param_list, - init_param_list, - package_properties, - doc_string, - data_structure_dict, - exgtype, - "exgtype", - "exgtype", - build_doc_string( - "exgtype", - "", - "is the exchange type (GWF-GWF or GWF-GWT).", - indent, - ), - None, - None, - True, - ) - add_var( - init_vars, - None, - options_param_list, - init_param_list, - package_properties, - doc_string, - data_structure_dict, - None, - "exgmnamea", - "exgmnamea", - build_doc_string( - "exgmnamea", - "", - "is the name of the first model that is " - "part of this exchange.", - indent, - ), - None, - None, - True, - ) - add_var( - init_vars, - None, - options_param_list, - init_param_list, - package_properties, - doc_string, - data_structure_dict, - None, - "exgmnameb", - "exgmnameb", - build_doc_string( - "exgmnameb", - "", - "is the name of the second model that is " - "part of this exchange.", - indent, - ), - None, - None, - True, - ) - init_vars.append( - " simulation.register_exchange_file(self)\n" - ) - - # loop through all blocks - for block in package[0].blocks.values(): - for data_structure in block.data_structures.values(): - # only create one property for each unique data structure name - if data_structure.name not in data_structure_dict: - mf_sim = ( - "parent_name_type" in package[0].header - and package[0].header["parent_name_type"][1] - == "MFSimulation" - ) - mf_nam = package[0].file_type == "nam" - if ( - data_structure.construct_package is not None - and not mf_sim - and not mf_nam - ): - c_pkg = data_structure.construct_package - else: - c_pkg = None - tg = add_var( - init_vars, - class_vars, - options_param_list, - init_param_list, - package_properties, - doc_string, - data_structure_dict, - data_structure.default_value, - data_structure.name, - data_structure.python_name, - data_structure.get_doc_string(79, indent, indent), - data_structure.path, - data_structure.get_datatype(), - False, - # c_pkg, - data_structure.construct_package, - data_structure.construct_data, - data_structure.parameter_name, - set_param_list, - mf_nam, - ) - if tg is not None and tg not in template_gens: - template_gens.append(tg) - - import_string = "from .. import mfpackage" - if template_gens: - import_string += "\nfrom ..data.mfdatautil import " - import_string += ", ".join(sorted(template_gens)) - # add extra docstrings for additional variables - doc_string.add_parameter( - " filename : String\n File name for this package." - ) - doc_string.add_parameter( - " pname : String\n Package name for this package." - ) - doc_string.add_parameter( - " parent_file : MFPackage\n " - "Parent package file that references this " - "package. Only needed for\n utility " - "packages (mfutl*). For example, mfutllaktab " - "package must have \n a mfgwflak " - "package parent_file." - ) - - # build package builder class string - init_vars.append(" self._init_complete = True") - init_vars = "\n".join(init_vars) - package_short_name = clean_class_string(package[0].file_type).lower() - class_def_string = "class Modflow{}(mfpackage.MFPackage):\n".format( - package_name.title() - ) - class_def_string = class_def_string.replace("-", "_") - class_var_string = ( - '{}\n package_abbr = "{}"\n _package_type = ' - '"{}"\n dfn_file_name = "{}"' - "\n".format( - "\n".join(class_vars), - package_abbr, - package[4], - package[0].dfn_file_name, - ) - ) - init_string_full = init_string_def - init_string_sim = f"{init_string_def}, simulation" - # add variables to init string - doc_string.add_parameter( - " loading_package : bool\n " - "Do not set this parameter. It is intended " - "for debugging and internal\n " - "processing purposes only.", - beginning_of_list=True, - ) - if "parent_name_type" in package[0].header: - init_var = package[0].header["parent_name_type"][0] - parent_type = package[0].header["parent_name_type"][1] - elif package[1] == PackageLevel.sim_level: - init_var = "simulation" - parent_type = "MFSimulation" - else: - init_var = "model" - parent_type = "MFModel" - doc_string.add_parameter( - f" {init_var} : {parent_type}\n " - f"{init_var.capitalize()} that this package is a part " - "of. Package is automatically\n " - f"added to {init_var} when it is " - "initialized.", - beginning_of_list=True, - ) - init_string_full = ( - f"{init_string_full}, {init_var}, loading_package=False" - ) - init_param_list.append("filename=None") - init_param_list.append("pname=None") - init_param_list.append("**kwargs") - init_string_full = build_init_string(init_string_full, init_param_list) - - # build init code - parent_init_string = " super().__init__(" - spaces = " " * len(parent_init_string) - parent_init_string = ( - '{}{}, "{}", filename, pname,\n{}' - "loading_package, **kwargs)\n\n" - " # set up variables".format( - parent_init_string, init_var, package_short_name, spaces - ) - ) - local_datetime = datetime.datetime.now(datetime.timezone.utc) - comment_string = ( - "# DO NOT MODIFY THIS FILE DIRECTLY. THIS FILE " - "MUST BE CREATED BY\n# mf6/utils/createpackages.py\n" - "# FILE created on {} UTC".format( - local_datetime.strftime("%B %d, %Y %H:%M:%S") - ) - ) - # assemble full package string - package_string = "{}\n{}\n\n\n{}{}\n{}\n{}\n\n{}{}\n{}\n".format( - comment_string, - import_string, - class_def_string, - doc_string.get_doc_string(), - class_var_string, - dfn_string, - init_string_full, - parent_init_string, - init_vars, - ) - - # open new Packages file - pb_file = open( - os.path.join(util_path, "..", "modflow", f"mf{package_name}.py"), - "w", - newline="\n", - ) - pb_file.write(package_string) - if ( - package[0].sub_package - and package_abbr != "utltab" - and ( - "parent_name_type" not in package[0].header - or package[0].header["parent_name_type"][1] != "MFSimulation" - ) - ): - set_param_list.append("filename=filename") - set_param_list.append("pname=pname") - set_param_list.append("child_builder_call=True") - whsp_1 = " " - whsp_2 = " " - - file_prefix = package[0].dfn_file_name[0:3] - chld_doc_string = ( - ' """\n {}Packages is a container ' - "class for the Modflow{} class.\n\n " - "Methods\n ----------" - "\n".format(package_name.title(), package_name.title()) - ) - - # write out child packages class - chld_cls = ( - "\n\nclass {}Packages(mfpackage.MFChildPackage" "s):\n".format( - package_name.title() - ) - ) - chld_var = ( - f" package_abbr = " - f'"{package_name.title().lower()}packages"\n\n' - ) - chld_init = " def initialize(self" - chld_init = build_init_string( - chld_init, init_param_list[:-1], whsp_1 - ) - init_pkg = "\n self.init_package(new_package, filename)" - params_init = ( - " new_package = Modflow" - f"{package_name.title()}(self._cpparent" - ) - params_init = build_init_string( - params_init, set_param_list, whsp_2 - ) - chld_doc_string = ( - "{} initialize\n Initializes a new " - "Modflow{} package removing any sibling " - "child\n packages attached to the same " - "parent package. See Modflow{} init\n " - " documentation for definition of " - "parameters.\n".format( - chld_doc_string, package_name.title(), package_name.title() - ) - ) - - chld_appn = "" - params_appn = "" - append_pkg = "" - if package_abbr != "utlobs": # Hard coded obs no multi-pkg support - chld_appn = "\n\n def append_package(self" - chld_appn = build_init_string( - chld_appn, init_param_list[:-1], whsp_1 - ) - append_pkg = ( - "\n self._append_package(new_package, filename)" - ) - params_appn = ( - " new_package = Modflow" - f"{file_prefix.capitalize()}" - f"{package_short_name}(self._cpparent" - ) - params_appn = build_init_string( - params_appn, set_param_list, whsp_2 - ) - chld_doc_string = ( - "{} append_package\n Adds a " - "new Modflow{}{} package to the container." - " See Modflow{}{}\n init " - "documentation for definition of " - "parameters.\n".format( - chld_doc_string, - file_prefix.capitalize(), - package_short_name, - file_prefix.capitalize(), - package_short_name, - ) - ) - chld_doc_string = f'{chld_doc_string} """\n' - packages_str = "{}{}{}{}{}{}{}{}{}\n".format( - chld_cls, - chld_doc_string, - chld_var, - chld_init, - params_init[:-2], - init_pkg, - chld_appn, - params_appn[:-2], - append_pkg, - ) - pb_file.write(packages_str) - pb_file.close() - - init_file_imports.append( - f"from .mf{package_name} import Modflow{package_name.title()}\n" - ) - - if package[0].dfn_type == mfstructure.DfnType.model_name_file: - # build model file - init_vars = build_model_init_vars(options_param_list) - - options_param_list.insert(0, "model_rel_path='.'") - options_param_list.insert(0, "exe_name='mf6'") - options_param_list.insert(0, "version='mf6'") - options_param_list.insert(0, "model_nam_file=None") - options_param_list.insert(0, "modelname='model'") - options_param_list.append("**kwargs,") - init_string_sim = build_init_string( - init_string_sim, options_param_list - ) - sim_name = clean_class_string(package[2]) - class_def_string = "class Modflow{}(mfmodel.MFModel):\n".format( - sim_name.capitalize() - ) - class_def_string = class_def_string.replace("-", "_") - doc_string.add_parameter( - " sim : MFSimulation\n " - "Simulation that this model is a part " - "of. Model is automatically\n " - "added to simulation when it is " - "initialized.", - beginning_of_list=True, - model_parameter=True, - ) - doc_string.description = ( - f"Modflow{sim_name} defines a {sim_name} model" - ) - class_var_string = f" model_type = '{sim_name}'\n" - mparent_init_string = " super().__init__(" - spaces = " " * len(mparent_init_string) - mparent_init_string = ( - "{}simulation, model_type='{}6',\n{}" - "modelname=modelname,\n{}" - "model_nam_file=model_nam_file,\n{}" - "version=version, exe_name=exe_name,\n{}" - "model_rel_path=model_rel_path,\n{}" - "**kwargs," - ")\n".format( - mparent_init_string, - sim_name, - spaces, - spaces, - spaces, - spaces, - spaces, - ) - ) - load_txt, doc_text = build_model_load(sim_name) - package_string = "{}\n{}\n\n\n{}{}\n{}\n{}\n{}{}\n{}\n\n{}".format( - comment_string, - nam_import_string, - class_def_string, - doc_string.get_doc_string(True), - doc_text, - class_var_string, - init_string_sim, - mparent_init_string, - init_vars, - load_txt, - ) - md_file = open( - os.path.join(util_path, "..", "modflow", f"mf{sim_name}.py"), - "w", - newline="\n", - ) - md_file.write(package_string) - md_file.close() - init_file_imports.append( - f"from .mf{sim_name} import Modflow{sim_name.capitalize()}\n" - ) - elif package[0].dfn_type == mfstructure.DfnType.sim_name_file: - # build simulation file - init_vars = build_model_init_vars(options_param_list) - - options_param_list.insert(0, "lazy_io=False") - options_param_list.insert(0, "use_pandas=True") - options_param_list.insert(0, "write_headers=True") - options_param_list.insert(0, "verbosity_level=1") - options_param_list.insert( - 0, "sim_ws: Union[str, os.PathLike] = " "os.curdir" - ) - options_param_list.insert( - 0, "exe_name: Union[str, os.PathLike] " '= "mf6"' - ) - options_param_list.insert(0, "version='mf6'") - options_param_list.insert(0, "sim_name='sim'") - init_string_sim = " def __init__(self" - init_string_sim = build_init_string( - init_string_sim, options_param_list - ) - class_def_string = ( - "class MFSimulation(mfsimbase." "MFSimulationBase):\n" - ) - doc_string.add_parameter( - " sim_name : str\n" " Name of the simulation", - beginning_of_list=True, - model_parameter=True, - ) - doc_string.description = ( - "MFSimulation is used to load, build, and/or save a MODFLOW " - "6 simulation. \n A MFSimulation object must be created " - "before creating any of the MODFLOW 6 \n model objects." - ) - sparent_init_string = " super().__init__(" - spaces = " " * len(sparent_init_string) - sparent_init_string = ( - "{}sim_name=sim_name,\n{}" - "version=version,\n{}" - "exe_name=exe_name,\n{}" - "sim_ws=sim_ws,\n{}" - "verbosity_level=verbosity_level,\n{}" - "write_headers=write_headers,\n{}" - "lazy_io=lazy_io,\n{}" - "use_pandas=use_pandas,\n{}" - ")\n".format( - sparent_init_string, - spaces, - spaces, - spaces, - spaces, - spaces, - spaces, - spaces, - spaces, - ) - ) - sim_import_string = ( - "import os\n" - "from typing import Union\n" - "from .. import mfsimbase" - ) - - load_txt, doc_text = build_sim_load() - package_string = "{}\n{}\n\n\n{}{}\n{}\n{}{}\n{}\n\n{}".format( - comment_string, - sim_import_string, - class_def_string, - doc_string.get_doc_string(False, True), - doc_text, - init_string_sim, - sparent_init_string, - init_vars, - load_txt, - ) - sim_file = open( - os.path.join(util_path, "..", "modflow", "mfsimulation.py"), - "w", - newline="\n", - ) - sim_file.write(package_string) - sim_file.close() - init_file_imports.append( - "from .mfsimulation import MFSimulation\n" - ) - - # Sort the imports - for line in sorted(init_file_imports, key=lambda x: x.split()[3]): - init_file.write(line) - init_file.close() +_MF6_PATH = Path(__file__).parents[1] if __name__ == "__main__": - create_packages() + from flopy.mf6.utils.codegen import make_all + make_all(dfndir=_MF6_PATH / "data" / "dfn", outdir=_MF6_PATH / "modflow") diff --git a/flopy/mf6/utils/generate_classes.py b/flopy/mf6/utils/generate_classes.py index 2dbf292ba1..ce5d08b3d9 100644 --- a/flopy/mf6/utils/generate_classes.py +++ b/flopy/mf6/utils/generate_classes.py @@ -5,8 +5,6 @@ from pathlib import Path from warnings import warn -from .createpackages import create_packages - thisfilepath = os.path.dirname(os.path.abspath(__file__)) flopypth = os.path.join(thisfilepath, "..", "..") flopypth = os.path.abspath(flopypth) @@ -14,6 +12,11 @@ default_owner = "MODFLOW-ORG" default_repo = "modflow6" +_MF6_PATH = Path(__file__).parents[1] +_DFN_PATH = _MF6_PATH / "data" / "dfn" +_TOML_PATH = _DFN_PATH / "toml" +_TGT_PATH = _MF6_PATH / "modflow" + def delete_files(files, pth, allow_failure=False, exclude=None): if exclude is None: @@ -198,8 +201,15 @@ def generate_classes( print(" Deleting existing mf6 classes") delete_mf6_classes() - print(" Creating mf6 classes using the definition files") - create_packages() + # convert dfns to toml.. when we + # do this upstream, remove this. + _TOML_PATH.mkdir(exist_ok=True) + from modflow_devtools.dfn2toml import convert as dfn2toml + dfn2toml(_DFN_PATH, _TOML_PATH) + + print(" Create mf6 classes using the definition files.") + from flopy.mf6.utils.codegen import make_all + make_all(_TOML_PATH, _TGT_PATH, version=2) list_files(os.path.join(flopypth, "mf6", "modflow")) diff --git a/pyproject.toml b/pyproject.toml index cc557a5758..86f0e54cd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,13 +29,17 @@ requires-python = ">=3.9" dependencies = [ "numpy>=1.20.3", "matplotlib >=1.4.0", - "pandas >=2.0.0" + "pandas >=2.0.0", ] dynamic = ["version", "readme"] [project.optional-dependencies] -dev = ["flopy[lint,test,optional,doc]", "tach"] -lint = ["ruff"] +dev = ["flopy[codegen,lint,test,optional,doc]", "tach"] +codegen = [ + "Jinja2>=3.0", + "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-USGS/modflow-devtools.git", +] +lint = ["cffconvert", "codespell[toml] >=2.2.2", "ruff"] test = [ "flopy[lint]", "coverage !=7.6.5", @@ -44,7 +48,7 @@ test = [ "jupyter", "jupyter_client >=8.4.0", # avoid datetime.utcnow() deprecation warning "jupytext", - "modflow-devtools", + "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-USGS/modflow-devtools.git", "pytest !=8.1.0", "pytest-benchmark", "pytest-cov", From 80e2db7a58fb6905de89e7e7476087fccec5b1c7 Mon Sep 17 00:00:00 2001 From: w-bonelli Date: Fri, 7 Mar 2025 07:08:29 -0500 Subject: [PATCH 2/3] allow direct references --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 86f0e54cd0..16126e6283 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,10 @@ only-include = [ [tool.hatch.build.targets.wheel] packages = ["flopy"] +[tool.hatch.metadata] +# temporary, until new devtools release +allow-direct-references = true + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" From ad9e2e2a750ef79ee55e36685d5801ec9ae8b14c Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 7 Mar 2025 12:03:47 -0500 Subject: [PATCH 3/3] org naming --- etc/environment.yml | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/environment.yml b/etc/environment.yml index c9e1186992..710ceec258 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -16,7 +16,7 @@ dependencies: - tomli - tomli-w - pip: - - git+https://github.com/MODFLOW-USGS/modflow-devtools.git + - git+https://github.com/MODFLOW-ORG/modflow-devtools.git # lint - cffconvert diff --git a/pyproject.toml b/pyproject.toml index 16126e6283..5a833b131e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dynamic = ["version", "readme"] dev = ["flopy[codegen,lint,test,optional,doc]", "tach"] codegen = [ "Jinja2>=3.0", - "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-USGS/modflow-devtools.git", + "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-ORG/modflow-devtools.git", ] lint = ["cffconvert", "codespell[toml] >=2.2.2", "ruff"] test = [ @@ -48,7 +48,7 @@ test = [ "jupyter", "jupyter_client >=8.4.0", # avoid datetime.utcnow() deprecation warning "jupytext", - "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-USGS/modflow-devtools.git", + "modflow-devtools[dfn] @ git+https://github.com/MODFLOW-ORG/modflow-devtools.git", "pytest !=8.1.0", "pytest-benchmark", "pytest-cov",