Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(codegen/docs): add type hints #2317

Draft
wants to merge 46 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
86b4dda
some experimenting
wpbonelli Sep 20, 2024
2cefd73
cleaner impl, test more components, proper diagrams in mf6_dev_guide.md
wpbonelli Sep 25, 2024
05b1fdb
cleanup
wpbonelli Sep 25, 2024
4f19763
appease python 3.9?
wpbonelli Sep 25, 2024
205c182
ruff, kw/record tagging fix
wpbonelli Sep 25, 2024
fb68cae
common variable substitutions
wpbonelli Sep 25, 2024
b78dd38
reorg, prep for e2e testing of createpackages.py
wpbonelli Sep 26, 2024
a5a580c
cleanup, add jinja to pyproject.toml
wpbonelli Sep 26, 2024
5a8b1be
cleanup
wpbonelli Sep 26, 2024
47f04bd
exg handling, more reorg, misc fixes, todo: subpackages
wpbonelli Sep 26, 2024
a448908
slinging dictionaries no longer
wpbonelli Sep 27, 2024
2713059
subpackages working? much cleanup
wpbonelli Oct 1, 2024
4678597
cleanup
wpbonelli Oct 1, 2024
a7fb655
fix subpkg support, more cleanup
wpbonelli Oct 1, 2024
c056dab
named tuples for records
wpbonelli Oct 1, 2024
09d3dca
py39 back-compat...
wpbonelli Oct 1, 2024
76ef653
py39
wpbonelli Oct 1, 2024
f09086b
generate_classes usage
wpbonelli Oct 2, 2024
0e6aca8
fixes
wpbonelli Oct 2, 2024
3876316
ruff
wpbonelli Oct 2, 2024
6893f64
subpkg fixes
wpbonelli Oct 2, 2024
bf3f2ee
MFChildPackages container for subpkgs
wpbonelli Oct 2, 2024
15f21dc
write __init__.py too
wpbonelli Oct 2, 2024
5f72225
handle mfnam pkg correctly
wpbonelli Oct 2, 2024
576d989
minor fixes
wpbonelli Oct 2, 2024
b6a468d
cleanup
wpbonelli Oct 2, 2024
ac6842f
cleanup/fixes
wpbonelli Oct 2, 2024
90de4c2
only import MFSimulationBase if needed (avoid circularity)
wpbonelli Oct 2, 2024
3d59da9
fix lists of unions/tuples
wpbonelli Oct 2, 2024
d391220
deduplicate 'auxiliary' var names
wpbonelli Oct 3, 2024
2015907
fixes
wpbonelli Oct 3, 2024
abbd94e
about half of test_mf6.py passing
wpbonelli Oct 3, 2024
56f177c
extra subpkgs
wpbonelli Oct 3, 2024
4b285d5
ast equiv test, some mf6 tests still failing (adv pkgs?)
wpbonelli Oct 3, 2024
656ff89
no py38
wpbonelli Oct 3, 2024
f850921
appease codacy: use literal_eval, remove unused vars
wpbonelli Oct 3, 2024
c34e146
unused var
wpbonelli Oct 3, 2024
5b2e257
multidict to the rescue for duplicate var names
wpbonelli Oct 3, 2024
ede48dd
fix test
wpbonelli Oct 3, 2024
cd9cbcc
dfn fixes
wpbonelli Oct 3, 2024
8e445fd
fixes for mf6 tests.. 127 passing, 20 to go
wpbonelli Oct 3, 2024
951e930
ci
wpbonelli Oct 4, 2024
3d8c257
render-time transformations
wpbonelli Oct 4, 2024
5e46278
cleanup
wpbonelli Oct 4, 2024
befdfc7
module reorg
wpbonelli Oct 7, 2024
6a0dfac
continuing module reorg
wpbonelli Oct 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
python-version: [ 3.8, 3.9, "3.10", "3.11", "3.12" ]
python-version: [ 3.9, "3.10", "3.11", "3.12" ]
defaults:
run:
shell: bash -l {0}
Expand Down Expand Up @@ -181,10 +181,13 @@ jobs:
working-directory: autotest
run: |
pytest -v -m="not example" -n=auto --cov=flopy --cov-append --cov-report=xml --durations=0 --keep-failed=.failed --dist loadfile
coverage report
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Report coverage
working-directory: autotest
run: coverage report

- name: Upload failed test outputs
uses: actions/upload-artifact@v4
if: failure()
Expand Down
162 changes: 162 additions & 0 deletions autotest/test_codegen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import traceback
from ast import Assign, ClassDef, expr
from ast import parse as parse_ast
from pprint import pformat
from shutil import copytree
from typing import List, Union
from warnings import warn

import pytest
from modflow_devtools.misc import run_cmd

from autotest.conftest import get_project_root_path
from flopy.mf6.utils.codegen.context import get_context_names
from flopy.mf6.utils.codegen.dfn import Dfn
from flopy.mf6.utils.codegen.make import (
DfnName,
make_all,
make_context,
make_contexts,
make_targets,
)

PROJ_ROOT = get_project_root_path()
MF6_PATH = PROJ_ROOT / "flopy" / "mf6"
TGT_PATH = MF6_PATH / "modflow"
DFN_PATH = MF6_PATH / "data" / "dfn"
DFN_NAMES = [
dfn.stem
for dfn in DFN_PATH.glob("*.dfn")
if dfn.stem not in ["common", "flopy"]
]


@pytest.mark.parametrize(
"dfn, n_flat, n_params", [("gwf-ic", 2, 2), ("prt-prp", 40, 18)]
)
def test_make_context(dfn, n_flat, n_params):
with open(DFN_PATH / "common.dfn") as f:
commonvars = Dfn.load(f)

with open(DFN_PATH / f"{dfn}.dfn") as f:
dfn = DfnName(*dfn.split("-"))
definition = Dfn.load(f, name=dfn)

context_names = get_context_names(dfn)
context_name = context_names[0]
context = make_context(context_name, definition, commonvars)
assert len(context_names) == 1
assert len(context.variables) == n_params
assert len(context.definition.metadata) == n_flat + 1 # +1 for metadata


@pytest.mark.skip(reason="TODO")
@pytest.mark.parametrize("dfn_name", ["gwf-ic", "prt-prp", "gwf-nam"])
def test_make_contexts(dfn_name):
with open(DFN_PATH / "common.dfn") as f:
common = Dfn.load(f)

# TODO


@pytest.mark.parametrize("dfn_name", DFN_NAMES)
def test_make_targets(dfn_name, function_tmpdir):
with open(DFN_PATH / "common.dfn") as f:
common = Dfn.load(f)

with open(DFN_PATH / f"{dfn_name}.dfn", "r") as f:
dfn_name = DfnName(*dfn_name.split("-"))
dfn = Dfn.load(f, name=dfn_name)

make_targets(dfn, function_tmpdir, common=common)
for ctx_name in get_context_names(dfn_name):
run_cmd("ruff", "format", function_tmpdir, verbose=True)
run_cmd("ruff", "check", "--fix", function_tmpdir, verbose=True)
assert (function_tmpdir / ctx_name.target).is_file()


def test_make_all(function_tmpdir):
make_all(DFN_PATH, function_tmpdir, verbose=True)


def compare_ast(
node1: Union[expr, List[expr]], node2: Union[expr, List[expr]]
) -> bool:
t1 = type(node1)
t2 = type(node2)
if t1 is not t2:
print(f"type mismatch: {t1} != {t2}")
return False

if t1 is ClassDef:
assert t2 is ClassDef
assert node1.name == node2.name
for base1, base2 in zip(node1.bases, node2.bases):

def _id(b):
attrs = ["id", "name", "attr"]
for attr in attrs:
try:
return getattr(b, attr)
except:
pass
return None

assert _id(base1) == _id(base2)

body1, body2 = node1.body, node2.body
assert len(body1) == len(body2), f"body mismatch in {node1.name}"

for b1, b2 in zip(body1, body2):
if isinstance(b1, Assign):
assert isinstance(b2, Assign)
b1tgts = set(sorted([t.id for t in b1.targets]))
b2tgts = set(sorted([t.id for t in b2.targets]))
diff = b1tgts ^ b2tgts
if any(diff):
warn(
f"assignment targets don't match in {node1.name}\n"
f"=> symmetric difference:\n{pformat(diff)}\n"
f"=> prev - test:\n{pformat(b1tgts - b2tgts)}\n"
f"=> test - prev:\n{pformat(b2tgts - b1tgts)}\n"
)


def test_equivalence(function_tmpdir):
prev_dir = function_tmpdir / "prev"
test_dir = function_tmpdir / "test"
test_dir.mkdir()
copytree(TGT_PATH, prev_dir)
make_all(DFN_PATH, test_dir, verbose=True)
prev_files = list(prev_dir.glob("*.py"))
test_files = list(test_dir.glob("*.py"))
prev_names = set([p.name for p in prev_files])
test_names = set([p.name for p in test_files])
diff = prev_names ^ test_names
assert not any(diff), (
f"previous files don't match test files\n"
f"=> symmetric difference:\n{pformat(diff)}\n"
f"=> prev - test:\n{pformat(prev_names - test_names)}\n"
f"=> test - prev:\n{pformat(test_names - prev_names)}\n"
)
for prev_file, test_file in zip(prev_files, test_files):
prev = parse_ast(open(prev_file).read())
try:
test = parse_ast(open(test_file).read())
except:
raise ValueError(
f"Failed to parse {test_file}: {traceback.format_exc()}"
)
prev_classes = [n for n in prev.body if isinstance(n, ClassDef)]
test_classes = [n for n in test.body if isinstance(n, ClassDef)]
prev_clsnames = set([c.name for c in prev_classes])
test_clsnames = set([c.name for c in test_classes])
diff = prev_clsnames ^ test_clsnames
assert not any(diff), (
f"previous classes don't match test classes in {test_file.name}\n"
f"=> symmetric difference:\n{pformat(diff)}\n"
f"=> prev - test:\n{pformat(prev_clsnames - test_clsnames)}\n"
f"=> test - prev:\n{pformat(test_clsnames - prev_clsnames)}\n"
)
for prev_cls, test_cls in zip(prev_classes, test_classes):
compare_ast(prev_cls, test_cls)
22 changes: 22 additions & 0 deletions autotest/test_dfn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest

from autotest.conftest import get_project_root_path
from flopy.mf6.utils.codegen.dfn import Dfn
from flopy.mf6.utils.codegen.make import DfnName

PROJ_ROOT = get_project_root_path()
MF6_PATH = PROJ_ROOT / "flopy" / "mf6"
TGT_PATH = MF6_PATH / "modflow"
DFN_PATH = MF6_PATH / "data" / "dfn"
DFN_NAMES = [
dfn.stem
for dfn in DFN_PATH.glob("*.dfn")
if dfn.stem not in ["common", "flopy"]
]


@pytest.mark.parametrize("dfn_name", DFN_NAMES)
def test_load_dfn(dfn_name):
dfn_path = DFN_PATH / f"{dfn_name}.dfn"
with open(dfn_path, "r") as f:
dfn = Dfn.load(f, name=DfnName(*dfn_name.split("-")))
52 changes: 29 additions & 23 deletions docs/mf6_dev_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,44 @@ FPMF6 uses meta-data files located in flopy/mf6/data/dfn to define the model and

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.


***
MFStructure --+ MFSimulationStructure --+ MFModelStructure --+ MFInputFileStructure --+ MFBlockStructure --+ MFDataStructure --+ MFDataItemStructure

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.
***
```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
```

Figure 1: Generic data structure hierarchy. Connections show composition relationships.

Package and Data Base Classes
-----------------------------------------------

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).


***
MFPackage --+ MFBlock --+ MFData

MFPackage --+ MFInputFileStructure

MFBlock --+ MFBlockStructure

MFData --+ MFDataStructure

MFData --* MFArray --* MFTransientArray

MFData --* MFList --* MFTransientList

MFData --* MFScalar --* MFTransientScalar

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.

Expand Down
1 change: 1 addition & 0 deletions flopy/mf6/data/dfn/exg-gwfgwf.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ name cvoptions
type record variablecv dewatered
reader urword
optional true
class_attr false
longname vertical conductance options
description none

Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwe-lke.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ description real or character value that defines the temperature of external inf

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -451,7 +451,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwe-mwe.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ description real or character value that defines the injection solute temperatur

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -417,7 +417,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwe-sfe.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ description real or character value that defines the temperature of inflow $(^{\

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -450,7 +450,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwe-uze.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ description real or character value that states what fraction of the simulated u

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -408,7 +408,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwf-lak.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ description real or character value that defines the bed slope for the lake outl

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -854,7 +854,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
4 changes: 2 additions & 2 deletions flopy/mf6/data/dfn/gwf-maw.dfn
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ description height above the pump elevation (SCALING\_LENGTH). If the simulated

block period
name auxiliaryrecord
type record auxiliary auxname auxval
type record aux auxname auxval
shape
tagged
in_record true
Expand All @@ -732,7 +732,7 @@ longname
description

block period
name auxiliary
name aux
type keyword
shape
in_record true
Expand Down
Loading
Loading