-
Notifications
You must be signed in to change notification settings - Fork 6
Generate simulation, model, and package classes #176
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
Draft
deltamarnix
wants to merge
45
commits into
modflowpy:develop
Choose a base branch
from
deltamarnix:generate-classes
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 3020f88
Move files that will be generated to the modflow folder
deltamarnix 308512d
Add code generation code from flopy 3
deltamarnix 43b4b25
Move some code and fix import references
deltamarnix 34cc59f
Initial try for creating MFSimulation
deltamarnix c8033e0
Start working on package
deltamarnix 20f7943
Add converter to arrays
deltamarnix 60fdcef
Make multiple dims possible
deltamarnix 190cdb6
Add model
deltamarnix a42f362
Add inner classes
deltamarnix 2121e71
Correct types in docstrings
deltamarnix a065222
Add custom implementations for models, exchange, solutions in simulation
deltamarnix 2901221
Make the code work with newer modflow_devtools
deltamarnix 6a304d3
Add some todos
deltamarnix 8241a35
Change record with filein or fileout to a PathLike
deltamarnix d01bf52
update pixi lock
deltamarnix 9dc6aca
Add ruff formatting after generating
deltamarnix 7e748c0
Move description to template
deltamarnix 096a0b7
Remove has_optional_or_default
deltamarnix 6007565
Improve documentation
deltamarnix a928982
Fix tdis
deltamarnix 9e537cb
Remove legacy_dir
deltamarnix 1401aed
sim- are packages, not models
deltamarnix ec66c45
keyword arguments in make
deltamarnix d21d4f9
Generate recarray as record
deltamarnix a267100
Try to extract extra attributes for recarray
deltamarnix ec07923
Add special case for numpy arrays
deltamarnix b749202
Move private functions to top of file
deltamarnix c44a152
Relative path for dfn path in debugger
deltamarnix c7795de
Add dim() to dimensions instead of field()
deltamarnix 7cc73b3
Add maxbound hook and reader in array
deltamarnix d4c728d
Solutions are packages, not models
deltamarnix 740751b
Add post_init function to package
deltamarnix 11f7ea1
Use python -m ruff instead of ruff directly
deltamarnix 0293902
Add dfns that are of interest for the prototype
deltamarnix 6eaf96e
Move and rename gwf packages for easier comparison with the generated…
deltamarnix 82f4eb0
Update and commit uv.lock file
deltamarnix 3d428bd
Rename some more files
deltamarnix 130896b
Gwf model is now rendered correctly
deltamarnix 47479c4
Document the assumption that we make during code gen
deltamarnix 1471c33
Merge remote-tracking branch 'usgs/develop' into generate-classes
deltamarnix a5e6b6b
Add models and solutions in the simulation
deltamarnix ab1888d
Add from_grid, to_grid, from_time, to_time functions
deltamarnix ebe8c19
Only add update_maxbound if maxbound attribute exists
deltamarnix cc9c9b8
Fix custom docstrings in sim
deltamarnix File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,4 @@ venv/ | |
| # pixi environments | ||
| .pixi | ||
| *.egg-info | ||
|
|
||
| # uv lockfile | ||
| uv.lock | ||
| dfn/toml | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .generate_classes import generate_classes | ||
|
|
||
| __all__ = ["generate_classes"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}"' | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.