Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c6fb890
Ignore mypy cache and pycache in vs code explorer
deltamarnix Jul 11, 2025
3020f88
Move files that will be generated to the modflow folder
deltamarnix Jul 11, 2025
308512d
Add code generation code from flopy 3
deltamarnix Jul 11, 2025
43b4b25
Move some code and fix import references
deltamarnix Jul 11, 2025
34cc59f
Initial try for creating MFSimulation
deltamarnix Jul 12, 2025
c8033e0
Start working on package
deltamarnix Jul 12, 2025
20f7943
Add converter to arrays
deltamarnix Jul 12, 2025
60fdcef
Make multiple dims possible
deltamarnix Jul 12, 2025
190cdb6
Add model
deltamarnix Jul 14, 2025
a42f362
Add inner classes
deltamarnix Jul 14, 2025
2121e71
Correct types in docstrings
deltamarnix Jul 14, 2025
a065222
Add custom implementations for models, exchange, solutions in simulation
deltamarnix Jul 15, 2025
2901221
Make the code work with newer modflow_devtools
deltamarnix Jul 15, 2025
6a304d3
Add some todos
deltamarnix Jul 16, 2025
8241a35
Change record with filein or fileout to a PathLike
deltamarnix Jul 16, 2025
d01bf52
update pixi lock
deltamarnix Jul 16, 2025
9dc6aca
Add ruff formatting after generating
deltamarnix Jul 17, 2025
7e748c0
Move description to template
deltamarnix Jul 17, 2025
096a0b7
Remove has_optional_or_default
deltamarnix Jul 17, 2025
6007565
Improve documentation
deltamarnix Jul 17, 2025
a928982
Fix tdis
deltamarnix Jul 17, 2025
9e537cb
Remove legacy_dir
deltamarnix Jul 17, 2025
1401aed
sim- are packages, not models
deltamarnix Jul 17, 2025
ec66c45
keyword arguments in make
deltamarnix Jul 17, 2025
d21d4f9
Generate recarray as record
deltamarnix Jul 17, 2025
a267100
Try to extract extra attributes for recarray
deltamarnix Jul 17, 2025
ec07923
Add special case for numpy arrays
deltamarnix Jul 17, 2025
b749202
Move private functions to top of file
deltamarnix Jul 17, 2025
c44a152
Relative path for dfn path in debugger
deltamarnix Jul 17, 2025
c7795de
Add dim() to dimensions instead of field()
deltamarnix Jul 17, 2025
7cc73b3
Add maxbound hook and reader in array
deltamarnix Jul 17, 2025
d4c728d
Solutions are packages, not models
deltamarnix Jul 17, 2025
740751b
Add post_init function to package
deltamarnix Jul 17, 2025
11f7ea1
Use python -m ruff instead of ruff directly
deltamarnix Aug 13, 2025
0293902
Add dfns that are of interest for the prototype
deltamarnix Aug 13, 2025
6eaf96e
Move and rename gwf packages for easier comparison with the generated…
deltamarnix Aug 14, 2025
82f4eb0
Update and commit uv.lock file
deltamarnix Aug 14, 2025
3d428bd
Rename some more files
deltamarnix Aug 14, 2025
130896b
Gwf model is now rendered correctly
deltamarnix Aug 14, 2025
47479c4
Document the assumption that we make during code gen
deltamarnix Aug 15, 2025
1471c33
Merge remote-tracking branch 'usgs/develop' into generate-classes
deltamarnix Aug 15, 2025
a5e6b6b
Add models and solutions in the simulation
deltamarnix Aug 15, 2025
ab1888d
Add from_grid, to_grid, from_time, to_time functions
deltamarnix Aug 15, 2025
ebe8c19
Only add update_maxbound if maxbound attribute exists
deltamarnix Aug 15, 2025
cc9c9b8
Fix custom docstrings in sim
deltamarnix Aug 19, 2025
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
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,4 @@ venv/
# pixi environments
.pixi
*.egg-info

# uv lockfile
uv.lock
dfn/toml
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: generate-classes",
"type": "debugpy",
"request": "launch",
"module": "codegen.generate_classes",
"args": [
"--dfnpath=${workspaceFolder}/dfn/"
]
},
{
"name": "Python Debugger: Current File",
"type": "debugpy",
Expand Down
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"**/Thumbs.db": true,
".pixi": true,
".ruff_cache": true,
".pytest_cache": true
".pytest_cache": true,
"**/__pycache__": true,
".mypy_cache": true
},
}
3 changes: 3 additions & 0 deletions codegen/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .generate_classes import generate_classes

__all__ = ["generate_classes"]
230 changes: 230 additions & 0 deletions codegen/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import builtins
import keyword
import os
from enum import Enum
from os import PathLike
from typing import Any, ForwardRef, Literal, Optional, Union, cast, get_args, get_origin

import jinja2
import numpy as np
from boltons.iterutils import remap
from modflow_devtools.dfn import get_fields
from numpy.typing import NDArray


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 _python_type(attr: dict[str, Any]) -> type | ForwardRef:
"""
Get the Python type of the attribute, e.g. int, str, float,
list, dict, etc.
"""
types: dict[str, type] = {
"integer": int,
"real": float,
"double precision": float,
"string": str,
"keyword": bool,
}

# options with a shape are lists
if attr.get("shape", None) and attr["type"] == "string":
py_type: Any = NDArray[np.object_]
elif attr.get("shape", None) and attr["type"] in ["real", "double precision"]:
py_type = NDArray[np.float64]
elif attr.get("shape", None) and attr["type"] == "integer":
py_type = NDArray[np.int64]
elif attr["type"] == "record":
if any(field in attr["children"] for field in ["filein", "fileout"]):
py_type = os.PathLike
else:
py_type = ForwardRef(Filters.class_name(attr["name"]), is_argument=False, is_class=True)
elif "file" in attr["name"]:
py_type = PathLike
else:
# TODO: Error on unknown type
py_type = types.get(attr["type"], Any)

if attr.get("optional", False):
py_type = Optional[py_type]

return py_type


def _type_to_string(
t: type | ForwardRef,
*,
type_sep: Literal["|", "or"],
optional: Literal[", optional", "| None"],
) -> str:
"""Convert a type to its string representation.

Args:
t: The type to convert
type_sep: The separator to use for multiple types (either '|' or 'or')
optional: The string to append for optional types (either '| None' or ', optional')
"""

# Handle None type
if t is type(None):
return "None"

if type(t) is ForwardRef:
return t.__forward_arg__

# Handle basic types with __name__
if hasattr(t, "__name__") and not hasattr(t, "__origin__"):
return t.__name__

# Handle generic types and special forms
origin = get_origin(t)
args = get_args(t)

if origin is None:
# Fallback for types without origin
return getattr(t, "__name__", str(t))

# Handle Union types (including Optional)
if origin is Union:
if len(args) == 2 and type(None) in args:
# This is Optional[T] which is Union[T, None]
non_none_type = args[0] if args[1] is type(None) else args[1]
non_none_str = _type_to_string(non_none_type, type_sep=type_sep, optional=optional)
return f"{non_none_str}{optional}"
else:
# Regular Union
arg_strs = [_type_to_string(arg, type_sep=type_sep, optional=optional) for arg in args]
return f" {type_sep} ".join(arg_strs)

if origin is np.ndarray:
if args:
if len(args) >= 2:
# Extract the dtype from the second argument
dtype_arg = args[1].__args__[0]
else:
dtype_arg = args[0].__args__[0]
dtype_str = _type_to_string(dtype_arg, type_sep=type_sep, optional=optional)
return f"NDArray[np.{dtype_str}]"
return "NDArray"

# Handle other generic types (list, dict, NDArray, etc.)
if hasattr(origin, "__name__"):
origin_name = origin.__name__
if args:
arg_strs = [_type_to_string(arg, type_sep=type_sep, optional=optional) for arg in args]
return f"{origin_name}[{', '.join(arg_strs)}]"
return origin_name

# Fallback
return str(t)


class Filters:
@staticmethod
def attrs(dfn: dict) -> list[dict]:
"""
Map the context's input variables to corresponding class attributes, where applicable.
"""
return list(get_fields(dfn).values())

@staticmethod
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 keyword.iskeyword(v) or v in dir(builtins) else v).replace("-", "_")

@staticmethod
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

@staticmethod
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

@staticmethod
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

@staticmethod
def type_str(attr: dict[str, Any]) -> str:
py_type = _python_type(attr)
return _type_to_string(py_type, type_sep="|", optional="| None")

@staticmethod
def type_docstr(attr: dict[str, Any]) -> str:
py_type = _python_type(attr)
return _type_to_string(py_type, type_sep="or", optional=", optional")

@staticmethod
def class_name(name: str) -> str:
"""Convert a string to a valid Python class name.
The incoming name consists of snake_case or hyphened words.
The output is CamelCase.
"""
# Replace hyphens with underscores
name = name.replace("-", "_")
# capitalize each word and join them
return "".join(word.capitalize() for word in name.split("_"))

@staticmethod
@jinja2.pass_environment
def children(env: jinja2.Environment, dfn: dict[str, Any]) -> list[dict[str, Any]]:
result = []

def visit(_, k, v):
if isinstance(v, dict) and v.get("parent", None) == dfn["name"]:
result.append(v)
return True

tree = cast(dict, env.globals["dfn_tree"]).copy()
remap(tree, visit=visit)
return result

@staticmethod
def double_quote(str: str) -> str:
return f'"{str}"'
Loading
Loading