Skip to content

Commit c5b5a41

Browse files
authored
refactor(createpackages): use jinja for mf6 module code generation (#2333)
Reimplement createpackages.py with jinja. The aim is to match the old code generation capability without relying on mfstructure.py. This is a first step toward a leaner python representation of the mf6 input specification. Note: mfstructure.py is still used at runtime which will need to be unraveled in followup work. The new code generation machinery lives in flopy.mf6.utils.codegen. It includes a bunch of filters handling quirks of the generated classes which we can aim to eliminate in future. The templates should also get much simpler in future. The module now consumes TOML rather than legacy DFNs. modflow-devtools is used at code generation time to convert DFNs to TOML, then generate sources as before. The conversion happens behind the scenes and does not change anything for the user of the codegen utility. A new optional dependency group 'codegen' is introduced, with Jinja2 and modflow-devtools, which are required for the code generation utilities. These and some transitive dependencies (tomli/tomli_w) are added to environment.yml. Miscellaneous: - expand and add some mermaid diagrams to the MF6 module dev guide - minor fix in flopy/mf6/data/mfdatastorage.py to avoid referencing a variable before it exists - update DFNs as per MODFLOW-ORG/modflow6#2031
1 parent 96f3b49 commit c5b5a41

25 files changed

+1315
-1617
lines changed

.docs/md/generate_classes.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
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.
1212

13-
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):
13+
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.
14+
15+
**Note**: to use this functionality, the `codegen` optional dependency group must be installed.
1416

1517
```bash
1618
$ python -m flopy.mf6.utils.generate_classes

.github/workflows/examples.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ jobs:
3434
powershell
3535
3636
- name: Install FloPy
37-
run: pip install .
37+
run: |
38+
pip install .
39+
pip install ".[codegen]"
3840
3941
- name: OpenGL workaround on Linux
4042
if: runner.os == 'Linux'

.github/workflows/rtd.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ concurrency:
1717
cancel-in-progress: true
1818
jobs:
1919
set_options:
20-
name: Set release options
20+
name: Set options
2121
runs-on: ubuntu-22.04
2222
outputs:
2323
ref: ${{ steps.set_ref.outputs.ref }}

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,8 @@ app
105105
# DFN backups
106106
flopy/mf6/data/dfn_backup/
107107

108+
# DFN TOML dir
109+
flopy/mf6/data/toml/
110+
108111
# uv lockfile
109-
uv.lock
112+
uv.lock

docs/mf6_dev_guide.md

+56-24
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,79 @@
1-
Introduction
2-
-----------------------------------------------
1+
# Developing FloPy for MF6
32

4-
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.
3+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
4+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
55

6-
Package Meta-Data and Package Files
7-
-----------------------------------------------
6+
- [Introduction](#introduction)
7+
- [Code generation](#code-generation)
8+
- [Input specification](#input-specification)
89

9-
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.
10+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
1011

11-
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.
12+
## Introduction
1213

14+
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.
1315

14-
***
15-
MFStructure --+ MFSimulationStructure --+ MFModelStructure --+ MFInputFileStructure --+ MFBlockStructure --+ MFDataStructure --+ MFDataItemStructure
16+
## Code generation
1617

17-
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.
18-
***
18+
MODFLOW 6 describes its input specification with definition (DFN) files.
1919

20-
Package and Data Base Classes
21-
-----------------------------------------------
20+
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.
2221

23-
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).
22+
FloPy can generate a MODFLOW 6 compatibility layer for itself, given a set of definition files:
23+
24+
- `flopy/mf6/utils/createpackages.py`: assumes definition files are in `flopy/mf6/data/dfn`
25+
- `flopy/mf6/utils/generate_classes.py`: downloads DFNs then runs `createpackages.py`
26+
27+
For instance, to sync with DFNs from the MODFLOW 6 develop branch:
2428

29+
```shell
30+
python -m flopy.mf6.utils.generate_classes --ref develop --no-backup
31+
```
2532

26-
***
27-
MFPackage --+ MFBlock --+ MFData
33+
Generated files are created in `flopy/mf6/modflow/`.
2834

29-
MFPackage --+ MFInputFileStructure
35+
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).
3036

31-
MFBlock --+ MFBlockStructure
37+
**Note**: Code generation requires a few extra dependencies, grouped in the `codegen` optional dependency group: `Jinja2` and `modflow-devtools`.
3238

33-
MFData --+ MFDataStructure
39+
## Input specification
3440

35-
MFData --* MFArray --* MFTransientArray
41+
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.
3642

37-
MFData --* MFList --* MFTransientList
43+
```mermaid
44+
classDiagram
45+
MFStructure *-- "1" MFSimulationStructure : has
46+
MFSimulationStructure *-- "1+" MFModelStructure : has
47+
MFModelStructure *-- "1" MFInputFileStructure : has
48+
MFInputFileStructure *-- "1+" MFBlockStructure : has
49+
MFBlockStructure *-- "1+" MFDataStructure : has
50+
MFDataStructure *-- "1+" MFDataItemStructure : has
51+
```
3852

39-
MFData --* MFScalar --* MFTransientScalar
53+
Figure 1: Generic data structure hierarchy. Connections show composition relationships.
54+
55+
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).
4056

41-
MFTransientData --* MFTransientArray, MFTransientList, MFTransientScalar
57+
```mermaid
58+
classDiagram
59+
60+
MFPackage *-- "1+" MFBlock : has
61+
MFBlock *-- "1+" MFData : has
62+
MFPackage *-- "1" MFInputFileStructure : has
63+
MFBlock *-- "1" MFBlockStructure : has
64+
MFData *-- "1" MFDataStructure : has
65+
MFData <|-- MFArray
66+
MFArray <|-- MFTransientArray
67+
MFData <|-- MFList
68+
MFList <|-- MFTransientList
69+
MFData <|-- MFScalar
70+
MFScalar <|-- MFTransientScalar
71+
MFTransientData <|-- MFTransientArray
72+
MFTransientData <|-- MFTransientList
73+
MFTransientData <|-- MFTransientScalar
74+
```
4275
4376
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.
44-
***
4577

4678
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.
4779

etc/environment.yml

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ dependencies:
1010
- matplotlib>=1.4.0
1111
- pandas>=2.0.0
1212

13+
# codegen
14+
- boltons>=1.0
15+
- Jinja2>=3.0
16+
- tomli
17+
- tomli-w
18+
- pip:
19+
- git+https://github.com/MODFLOW-ORG/modflow-devtools.git
20+
1321
# lint
1422
- cffconvert
1523
- codespell>=2.2.2

flopy/mf6/data/dfn/utl-ts.dfn

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ valid stepwise linear linearend
6262
shape time_series_names
6363
tagged false
6464
reader urword
65+
in_record true
6566
optional false
6667
in_record true
6768
longname
@@ -112,6 +113,7 @@ name sfacs
112113
type keyword
113114
shape
114115
reader urword
116+
in_record true
115117
optional false
116118
in_record true
117119
longname

flopy/mf6/data/mfdatastorage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def __init__(
316316
self.data_structure_type = data_structure_type
317317
package_dim = self.data_dimensions.package_dim
318318
self.in_model = (
319-
self.data_dimensions is not None
319+
package_dim is not None
320320
and len(package_dim.package_path) > 1
321321
and package_dim.model_dim[0].model_name is not None
322322
and package_dim.model_dim[0].model_name.lower()

flopy/mf6/data/mfdatautil.py

-86
Original file line numberDiff line numberDiff line change
@@ -1054,89 +1054,3 @@ def empty(
10541054
return template
10551055
else:
10561056
return rec_array
1057-
1058-
1059-
class MFDocString:
1060-
"""
1061-
Helps build a python class doc string
1062-
1063-
Parameters
1064-
----------
1065-
description : string
1066-
description of the class
1067-
1068-
Attributes
1069-
----------
1070-
indent: string
1071-
indent to use in doc string
1072-
description : string
1073-
description of the class
1074-
parameter_header : string
1075-
header for parameter section of doc string
1076-
parameters : list
1077-
list of docstrings for class parameters
1078-
1079-
Methods
1080-
-------
1081-
add_parameter : (param_descr : string, beginning_of_list : bool)
1082-
adds doc string for a parameter with description 'param_descr' to the
1083-
end of the list unless beginning_of_list is True
1084-
get_doc_string : () : string
1085-
builds and returns the docstring for the class
1086-
"""
1087-
1088-
def __init__(self, description):
1089-
self.indent = " "
1090-
self.description = description
1091-
self.parameter_header = (
1092-
f"{self.indent}Parameters\n{self.indent}----------"
1093-
)
1094-
self.parameters = []
1095-
self.model_parameters = []
1096-
1097-
def add_parameter(
1098-
self, param_descr, beginning_of_list=False, model_parameter=False
1099-
):
1100-
if beginning_of_list:
1101-
self.parameters.insert(0, param_descr)
1102-
if model_parameter:
1103-
self.model_parameters.insert(0, param_descr)
1104-
else:
1105-
self.parameters.append(param_descr)
1106-
if model_parameter:
1107-
self.model_parameters.append(param_descr)
1108-
1109-
def get_doc_string(self, model_doc_string=False, sim_doc_string=False):
1110-
doc_string = '{}"""\n{}{}\n\n{}\n'.format(
1111-
self.indent, self.indent, self.description, self.parameter_header
1112-
)
1113-
if model_doc_string:
1114-
param_list = self.model_parameters
1115-
doc_string = (
1116-
"{} modelname : string\n name of the "
1117-
"model\n model_nam_file : string\n"
1118-
" relative path to the model name file from "
1119-
"model working folder\n version : string\n"
1120-
" version of modflow\n exe_name : string\n"
1121-
" model executable name\n"
1122-
" model_ws : string\n"
1123-
" model working folder path"
1124-
"\n".format(doc_string)
1125-
)
1126-
else:
1127-
param_list = self.parameters
1128-
for parameter in param_list:
1129-
if sim_doc_string:
1130-
pclean = parameter.strip()
1131-
if (
1132-
pclean.startswith("simulation")
1133-
or pclean.startswith("loading_package")
1134-
or pclean.startswith("filename")
1135-
or pclean.startswith("pname")
1136-
or pclean.startswith("parent_file")
1137-
):
1138-
continue
1139-
doc_string += f"{parameter}\n"
1140-
if not (model_doc_string or sim_doc_string):
1141-
doc_string += f'\n{self.indent}"""'
1142-
return doc_string

0 commit comments

Comments
 (0)