mfai-viewer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mfai_viewer-0.1.0/.gitignore +126 -0
- mfai_viewer-0.1.0/PKG-INFO +51 -0
- mfai_viewer-0.1.0/README.md +28 -0
- mfai_viewer-0.1.0/pyproject.toml +41 -0
- mfai_viewer-0.1.0/src/mfai_viewer/__init__.py +24 -0
- mfai_viewer-0.1.0/src/mfai_viewer/__main__.py +5 -0
- mfai_viewer-0.1.0/src/mfai_viewer/cli.py +190 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/__init__.py +0 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/__init__.py +0 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/__init__.py +0 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/types.py +12 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/types_mesh.py +169 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/types_params.py +120 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/types_source.py +119 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/cln/types_targets.py +132 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/exceptions.py +693 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/models/__init__.py +3 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/models/bc.py +960 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/models/config_base.py +48 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/models/config_package.py +567 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/__init__.py +280 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/base.py +75 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/import_capabilities/index.json +47 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/import_capabilities/mf2005.json +325 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/import_capabilities/mf6.json +2187 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/import_capabilities/mfusg.json +1276 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/import_capabilities.py +281 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/mf6_bc.py +234 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/mf6_core.py +260 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/mf6_hfb.py +34 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/mfusg_bc.py +24 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/mfusg_core.py +198 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/package_types/write_forms.py +92 -0
- mfai_viewer-0.1.0/src/mfai_viewer/gwlink/backend/core/viewer_snapshot.py +1012 -0
- mfai_viewer-0.1.0/src/mfai_viewer/sources.json +215 -0
- mfai_viewer-0.1.0/src/mfai_viewer/upload.py +124 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/__init__.py +22 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/fgb.py +164 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/flopy_routes.py +1902 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/isosurface.py +610 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/payloads3d.py +363 -0
- mfai_viewer-0.1.0/src/mfai_viewer/writer/snapshot.py +116 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-debug.log*
|
|
4
|
+
.yarn/*
|
|
5
|
+
!.yarn/releases
|
|
6
|
+
!.yarn/plugins
|
|
7
|
+
.yarnrc.yml
|
|
8
|
+
|
|
9
|
+
# Environment files - NEVER commit these!
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
*.env
|
|
14
|
+
**/.env
|
|
15
|
+
**/.env.local
|
|
16
|
+
|
|
17
|
+
# SSH keys - NEVER commit these!
|
|
18
|
+
*.pem
|
|
19
|
+
*.key
|
|
20
|
+
id_rsa*
|
|
21
|
+
id_ed25519*
|
|
22
|
+
*.ppk
|
|
23
|
+
|
|
24
|
+
# API tokens and secrets
|
|
25
|
+
*secret*
|
|
26
|
+
*token*
|
|
27
|
+
*apikey*
|
|
28
|
+
.secrets/
|
|
29
|
+
|
|
30
|
+
# Build outputs
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
33
|
+
.next/
|
|
34
|
+
.turbo/
|
|
35
|
+
*.tsbuildinfo
|
|
36
|
+
|
|
37
|
+
# Testing
|
|
38
|
+
.playwright-mcp/
|
|
39
|
+
playwright-report/
|
|
40
|
+
test-results/
|
|
41
|
+
coverage/
|
|
42
|
+
|
|
43
|
+
# Archive and reference directories
|
|
44
|
+
.archive/
|
|
45
|
+
.references/
|
|
46
|
+
.docs/
|
|
47
|
+
|
|
48
|
+
# Local development
|
|
49
|
+
.vscode/
|
|
50
|
+
.idea/
|
|
51
|
+
*.swp
|
|
52
|
+
*.swo
|
|
53
|
+
*~
|
|
54
|
+
|
|
55
|
+
# Logs
|
|
56
|
+
*.log
|
|
57
|
+
logs/
|
|
58
|
+
|
|
59
|
+
# OS files
|
|
60
|
+
.DS_Store
|
|
61
|
+
Thumbs.db
|
|
62
|
+
|
|
63
|
+
# Cloudflare Wrangler
|
|
64
|
+
.wrangler/
|
|
65
|
+
.dev.vars
|
|
66
|
+
|
|
67
|
+
# Compiled MCP tools
|
|
68
|
+
.tools-compiled/
|
|
69
|
+
stdio/dist/
|
|
70
|
+
stdio/stdio/
|
|
71
|
+
|
|
72
|
+
# Data directories
|
|
73
|
+
data/
|
|
74
|
+
examples/
|
|
75
|
+
flopy-tutorials-*/
|
|
76
|
+
scripts/modflow6_examples_data/
|
|
77
|
+
scripts/plots/
|
|
78
|
+
scripts/temp/
|
|
79
|
+
scripts/*.tif
|
|
80
|
+
scripts/*.json
|
|
81
|
+
|
|
82
|
+
# Temporary files
|
|
83
|
+
*.png
|
|
84
|
+
!mfai-chat/public/**/*.png
|
|
85
|
+
!assets/**/*.png
|
|
86
|
+
deploy_to_vm.py
|
|
87
|
+
deploy_to_vm.sh
|
|
88
|
+
update_vm_packages.sh
|
|
89
|
+
fix_*.py
|
|
90
|
+
|
|
91
|
+
# Auto-generated
|
|
92
|
+
next-env.d.ts
|
|
93
|
+
|
|
94
|
+
# Python
|
|
95
|
+
__pycache__/
|
|
96
|
+
*.py[cod]
|
|
97
|
+
*$py.class
|
|
98
|
+
*.so
|
|
99
|
+
.Python
|
|
100
|
+
env/
|
|
101
|
+
venv/
|
|
102
|
+
.venv/
|
|
103
|
+
|
|
104
|
+
# Ignore tutorial outputs and runtime files
|
|
105
|
+
examples/**/output/
|
|
106
|
+
examples/**/plots/
|
|
107
|
+
examples/**/*.png
|
|
108
|
+
examples/**/*.jpg
|
|
109
|
+
examples/**/*.csv
|
|
110
|
+
examples/**/simulation/
|
|
111
|
+
examples/**/__pycache__/
|
|
112
|
+
examples/**/*.pyc
|
|
113
|
+
|
|
114
|
+
# Ignore test/workspace directories
|
|
115
|
+
workspace/
|
|
116
|
+
test_tutorials/
|
|
117
|
+
tutorial_test_env/
|
|
118
|
+
fixed_tutorials/
|
|
119
|
+
fixed_scripts/
|
|
120
|
+
|
|
121
|
+
# But keep the Python files!
|
|
122
|
+
!examples/**/*.py
|
|
123
|
+
|
|
124
|
+
# Keep documentation
|
|
125
|
+
!*.md
|
|
126
|
+
python_executor/modflow-executor-with-tutorials.tar.gz
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mfai-viewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write and upload the MODFLOW AI web viewer snapshot of a MODFLOW 6 model that was run with FloPy
|
|
5
|
+
Project-URL: Homepage, https://www.modflow.ai
|
|
6
|
+
Project-URL: Source, https://github.com/modflowai/modflowai-mcp
|
|
7
|
+
Author-email: GWLab <admin@modflow.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: flopy,groundwater,modflow,viewer
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Hydrology
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: flatbuffers>=23.5
|
|
16
|
+
Requires-Dist: flopy>=3.5
|
|
17
|
+
Requires-Dist: matplotlib>=3.7
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: scipy>=1.10
|
|
21
|
+
Requires-Dist: shapely>=2.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# mfai-viewer
|
|
25
|
+
|
|
26
|
+
The writer behind the `open_in_viewer` tool of the MODFLOW AI MCP
|
|
27
|
+
(`https://mcp.modflow.ai`). It turns a MODFLOW 6 simulation that was built
|
|
28
|
+
with FloPy and run on this machine into the snapshot the MODFLOW AI web
|
|
29
|
+
viewer (`viewer.modflow.ai`) reads, and uploads that snapshot to the link
|
|
30
|
+
the MCP tool created.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
python -m pip install mfai-viewer
|
|
34
|
+
mfai-viewer snapshot --workdir <model dir> --out <snapshot dir>
|
|
35
|
+
mfai-viewer upload <snapshot dir> "<uploads url from open_in_viewer>"
|
|
36
|
+
mfai-viewer version
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`snapshot` refuses a model that has not been run (no head file). It writes
|
|
40
|
+
`manifest.json` and a small `summary.json`; the MCP tool takes the summary
|
|
41
|
+
and answers the one `upload` command to run. `upload` sends every file in
|
|
42
|
+
parallel, retries, marks the link complete and prints the URL.
|
|
43
|
+
|
|
44
|
+
Exit codes: 0 done, 2 the environment cannot run it (Python older than 3.10,
|
|
45
|
+
missing packages, bad arguments), 3 the model has not been run, 4 files are
|
|
46
|
+
still missing after the upload, 1 anything else.
|
|
47
|
+
|
|
48
|
+
The writer code (`mfai_viewer/writer`) and the GWlink slice it vendors
|
|
49
|
+
(`mfai_viewer/gwlink`) are copied byte for byte from the mfai-chat and gwlink
|
|
50
|
+
repositories; `mfai_viewer/sources.json` records the commits and the sha256
|
|
51
|
+
of every file. Nothing is edited here.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# mfai-viewer
|
|
2
|
+
|
|
3
|
+
The writer behind the `open_in_viewer` tool of the MODFLOW AI MCP
|
|
4
|
+
(`https://mcp.modflow.ai`). It turns a MODFLOW 6 simulation that was built
|
|
5
|
+
with FloPy and run on this machine into the snapshot the MODFLOW AI web
|
|
6
|
+
viewer (`viewer.modflow.ai`) reads, and uploads that snapshot to the link
|
|
7
|
+
the MCP tool created.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
python -m pip install mfai-viewer
|
|
11
|
+
mfai-viewer snapshot --workdir <model dir> --out <snapshot dir>
|
|
12
|
+
mfai-viewer upload <snapshot dir> "<uploads url from open_in_viewer>"
|
|
13
|
+
mfai-viewer version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`snapshot` refuses a model that has not been run (no head file). It writes
|
|
17
|
+
`manifest.json` and a small `summary.json`; the MCP tool takes the summary
|
|
18
|
+
and answers the one `upload` command to run. `upload` sends every file in
|
|
19
|
+
parallel, retries, marks the link complete and prints the URL.
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 done, 2 the environment cannot run it (Python older than 3.10,
|
|
22
|
+
missing packages, bad arguments), 3 the model has not been run, 4 files are
|
|
23
|
+
still missing after the upload, 1 anything else.
|
|
24
|
+
|
|
25
|
+
The writer code (`mfai_viewer/writer`) and the GWlink slice it vendors
|
|
26
|
+
(`mfai_viewer/gwlink`) are copied byte for byte from the mfai-chat and gwlink
|
|
27
|
+
repositories; `mfai_viewer/sources.json` records the commits and the sha256
|
|
28
|
+
of every file. Nothing is edited here.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mfai-viewer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Write and upload the MODFLOW AI web viewer snapshot of a MODFLOW 6 model that was run with FloPy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "GWLab", email = "admin@modflow.ai" }]
|
|
13
|
+
keywords = ["modflow", "flopy", "groundwater", "viewer"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Hydrology",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=1.24",
|
|
22
|
+
"flopy>=3.5",
|
|
23
|
+
"flatbuffers>=23.5",
|
|
24
|
+
"pydantic>=2.0",
|
|
25
|
+
"scipy>=1.10",
|
|
26
|
+
"matplotlib>=3.7",
|
|
27
|
+
"shapely>=2.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://www.modflow.ai"
|
|
32
|
+
Source = "https://github.com/modflowai/modflowai-mcp"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
mfai-viewer = "mfai_viewer.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/mfai_viewer"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = ["src/mfai_viewer", "README.md", "pyproject.toml"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""mfai-viewer: the MODFLOW AI web viewer snapshot of a MODFLOW 6 model run with FloPy.
|
|
2
|
+
|
|
3
|
+
The writer itself is :mod:`mfai_viewer.writer`, a byte for byte copy of the
|
|
4
|
+
browser writer in the mfai-chat repository, with the GWlink slice it vendors
|
|
5
|
+
under :mod:`mfai_viewer.gwlink`. ``sources.json`` next to this file records
|
|
6
|
+
the commits and the sha256 of every copied file. This package adds the
|
|
7
|
+
version, the console script (``mfai-viewer snapshot | upload | version``)
|
|
8
|
+
and the uploader; nothing in the writer is edited here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .writer import ModelNotRunError, write_snapshot # noqa: F401
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
__all__ = ["ModelNotRunError", "write_snapshot", "__version__", "sources"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sources() -> dict:
|
|
23
|
+
"""The commits and file hashes the package was built from."""
|
|
24
|
+
return json.loads((Path(__file__).parent / "sources.json").read_text())
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""The ``mfai-viewer`` command: ``snapshot``, ``upload``, ``version``.
|
|
2
|
+
|
|
3
|
+
mfai-viewer snapshot --workdir <model dir> --out <snapshot dir>
|
|
4
|
+
mfai-viewer upload <snapshot dir> "<uploads url from open_in_viewer>"
|
|
5
|
+
mfai-viewer version
|
|
6
|
+
|
|
7
|
+
``snapshot`` writes the viewer snapshot of a MODFLOW 6 simulation that was
|
|
8
|
+
run (``mfsim.nam`` plus a head file) and a small ``summary.json`` the MCP
|
|
9
|
+
tool ``open_in_viewer`` takes as its argument. ``upload`` sends the files to
|
|
10
|
+
the link that tool created and prints the URL.
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 done, 2 the environment cannot run it (Python version, missing
|
|
13
|
+
packages, bad arguments), 3 the model has not been run, 4 files are still
|
|
14
|
+
missing after the upload, 1 anything else.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
|
|
26
|
+
PACKAGE = "mfai-viewer"
|
|
27
|
+
REQUIRED = ["numpy", "flopy", "flatbuffers", "pydantic", "scipy", "matplotlib", "shapely"]
|
|
28
|
+
MIN_PYTHON = (3, 10)
|
|
29
|
+
SUMMARY_FILE = "summary.json"
|
|
30
|
+
EXIT_ENVIRONMENT = 2
|
|
31
|
+
EXIT_NOT_RUN = 3
|
|
32
|
+
EXIT_INCOMPLETE = 4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_environment() -> list[str]:
|
|
36
|
+
missing = []
|
|
37
|
+
for name in REQUIRED:
|
|
38
|
+
try:
|
|
39
|
+
__import__(name)
|
|
40
|
+
except ImportError:
|
|
41
|
+
missing.append(name)
|
|
42
|
+
return missing
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def version_info() -> dict:
|
|
46
|
+
from . import sources
|
|
47
|
+
|
|
48
|
+
src = sources()
|
|
49
|
+
return {
|
|
50
|
+
"name": PACKAGE,
|
|
51
|
+
"version": __version__,
|
|
52
|
+
"python": sys.version.split()[0],
|
|
53
|
+
"sources": src.get("sources", {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_summary(out: Path, report: dict, workdir: Path) -> dict:
|
|
58
|
+
"""Stamp the writer into ``manifest.json`` and write ``summary.json``.
|
|
59
|
+
|
|
60
|
+
The manifest gains ``writer: {name, version}`` (the link service and the
|
|
61
|
+
web build ignore keys they do not read). The summary is what the agent
|
|
62
|
+
hands to ``open_in_viewer``: a few KB, not the whole manifest. The link
|
|
63
|
+
service creates one upload slot per ``files`` entry plus one for the
|
|
64
|
+
manifest itself, whose sha256 it verifies on upload.
|
|
65
|
+
"""
|
|
66
|
+
manifest_path = out / "manifest.json"
|
|
67
|
+
manifest = json.loads(manifest_path.read_text())
|
|
68
|
+
manifest["writer"] = {"name": PACKAGE, "version": __version__}
|
|
69
|
+
manifest_path.write_text(json.dumps(manifest, indent=1))
|
|
70
|
+
manifest_bytes = manifest_path.read_bytes()
|
|
71
|
+
files = [[f["key"], int(f["bytes"])] for f in manifest["files"]]
|
|
72
|
+
results = manifest.get("results") or {}
|
|
73
|
+
summary = {
|
|
74
|
+
"schemaVersion": manifest["schemaVersion"],
|
|
75
|
+
"writer": manifest["writer"],
|
|
76
|
+
"model": {k: manifest["model"].get(k) for k in ("name", "type", "ncpl", "nlay", "nper")},
|
|
77
|
+
"cells": report["cells"],
|
|
78
|
+
"results": {
|
|
79
|
+
"included": bool(results.get("included")),
|
|
80
|
+
"timestepsIncluded": len(results.get("timestepsIncluded") or []),
|
|
81
|
+
"timestepsTotal": int(results.get("timestepsTotal") or 0),
|
|
82
|
+
},
|
|
83
|
+
"manifest": {"bytes": len(manifest_bytes), "sha256": hashlib.sha256(manifest_bytes).hexdigest()},
|
|
84
|
+
"files": files,
|
|
85
|
+
"bytes": sum(b for _, b in files),
|
|
86
|
+
"snapshotDir": str(out),
|
|
87
|
+
"workdir": str(workdir),
|
|
88
|
+
}
|
|
89
|
+
(out / SUMMARY_FILE).write_text(json.dumps(summary, separators=(",", ":")) + "\n")
|
|
90
|
+
return summary
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_snapshot(args: argparse.Namespace) -> int:
|
|
94
|
+
if sys.version_info < MIN_PYTHON:
|
|
95
|
+
print(f"{PACKAGE} needs Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or newer; this is {sys.version.split()[0]}. "
|
|
96
|
+
f"Install it in a newer interpreter: python3.12 -m pip install {PACKAGE}", file=sys.stderr)
|
|
97
|
+
return EXIT_ENVIRONMENT
|
|
98
|
+
missing = check_environment()
|
|
99
|
+
if missing:
|
|
100
|
+
print(f"Missing Python packages: {' '.join(missing)}. Reinstall {PACKAGE} in this interpreter so pip brings them:\n"
|
|
101
|
+
f" {sys.executable} -m pip install --force-reinstall {PACKAGE}", file=sys.stderr)
|
|
102
|
+
return EXIT_ENVIRONMENT
|
|
103
|
+
|
|
104
|
+
workdir = Path(args.workdir).expanduser().resolve()
|
|
105
|
+
out = Path(args.out).expanduser().resolve()
|
|
106
|
+
if not workdir.is_dir():
|
|
107
|
+
print(f"no such directory: {workdir}", file=sys.stderr)
|
|
108
|
+
return EXIT_ENVIRONMENT
|
|
109
|
+
if not (workdir / "mfsim.nam").is_file():
|
|
110
|
+
print(f"{workdir} holds no mfsim.nam: pass the directory of the MODFLOW 6 simulation that was run.", file=sys.stderr)
|
|
111
|
+
return EXIT_ENVIRONMENT
|
|
112
|
+
if out.exists() and any(out.iterdir()):
|
|
113
|
+
print(f"{out} is not empty: pass a new or empty --out directory.", file=sys.stderr)
|
|
114
|
+
return EXIT_ENVIRONMENT
|
|
115
|
+
|
|
116
|
+
from .writer import ModelNotRunError, write_snapshot
|
|
117
|
+
|
|
118
|
+
kwargs = {"iso_surface_steps": args.iso_surfaces}
|
|
119
|
+
if args.max_head_files is not None:
|
|
120
|
+
kwargs["max_head_files"] = args.max_head_files
|
|
121
|
+
try:
|
|
122
|
+
report = write_snapshot(str(workdir), str(out), **kwargs)
|
|
123
|
+
except ModelNotRunError:
|
|
124
|
+
# The writer's own sentence names the browser's run_modflow; here the run is MODFLOW 6 on this machine.
|
|
125
|
+
print(f"This model has not been run yet: {workdir} holds no head file with timesteps. Run MODFLOW 6 on it first, then write the snapshot.", file=sys.stderr)
|
|
126
|
+
return EXIT_NOT_RUN
|
|
127
|
+
|
|
128
|
+
summary = write_summary(out, report, workdir)
|
|
129
|
+
print(json.dumps(summary, separators=(",", ":")))
|
|
130
|
+
print(f"snapshot written: {len(summary['files'])} files, {summary['bytes']} bytes, {summary['cells']} cells, "
|
|
131
|
+
f"{summary['results']['timestepsIncluded']}/{summary['results']['timestepsTotal']} timesteps, "
|
|
132
|
+
f"{report['write_seconds']:.1f} s. Next: call the MCP tool open_in_viewer with {out / SUMMARY_FILE}.", file=sys.stderr)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_upload(args: argparse.Namespace) -> int:
|
|
137
|
+
from .upload import UploadError, upload_snapshot
|
|
138
|
+
|
|
139
|
+
snapshot = Path(args.snapshot).expanduser().resolve()
|
|
140
|
+
if not (snapshot / "manifest.json").is_file():
|
|
141
|
+
print(f"{snapshot} holds no manifest.json: pass the --out directory of mfai-viewer snapshot.", file=sys.stderr)
|
|
142
|
+
return EXIT_ENVIRONMENT
|
|
143
|
+
try:
|
|
144
|
+
result = upload_snapshot(snapshot, args.uploads_url, workers=args.workers, log=lambda line: print(line, file=sys.stderr))
|
|
145
|
+
except UploadError as err:
|
|
146
|
+
print(str(err), file=sys.stderr)
|
|
147
|
+
return EXIT_INCOMPLETE if err.missing else 1
|
|
148
|
+
print(json.dumps(result, indent=1))
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_version(_args: argparse.Namespace) -> int:
|
|
153
|
+
print(json.dumps(version_info(), indent=1))
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
158
|
+
parser = argparse.ArgumentParser(prog=PACKAGE, description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
159
|
+
parser.add_argument("--version", action="version", version=f"{PACKAGE} {__version__}")
|
|
160
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
161
|
+
|
|
162
|
+
snap = sub.add_parser("snapshot", help="write the viewer snapshot of a MODFLOW 6 simulation that was run")
|
|
163
|
+
snap.add_argument("--workdir", required=True, help="directory of the MODFLOW 6 simulation (mfsim.nam) after it ran")
|
|
164
|
+
snap.add_argument("--out", required=True, help="directory to write the snapshot into (created; must be empty or absent)")
|
|
165
|
+
snap.add_argument("--max-head-files", type=int, default=None, help="cap on heads files (layers x timesteps); default 90")
|
|
166
|
+
snap.add_argument("--iso-surfaces", choices=["last", "all", "none"], default="last", help="which timesteps get an iso-surface payload")
|
|
167
|
+
snap.set_defaults(func=cmd_snapshot)
|
|
168
|
+
|
|
169
|
+
up = sub.add_parser("upload", help="send a snapshot to the link open_in_viewer created and print the URL")
|
|
170
|
+
up.add_argument("snapshot", help="the snapshot directory (--out of mfai-viewer snapshot)")
|
|
171
|
+
up.add_argument("uploads_url", help="the uploads URL open_in_viewer answered")
|
|
172
|
+
up.add_argument("--workers", type=int, default=8, help="files in flight at once; default 8")
|
|
173
|
+
up.set_defaults(func=cmd_upload)
|
|
174
|
+
|
|
175
|
+
ver = sub.add_parser("version", help="print the package version and the source commits")
|
|
176
|
+
ver.set_defaults(func=cmd_version)
|
|
177
|
+
return parser
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main(argv: list[str] | None = None) -> int:
|
|
181
|
+
parser = build_parser()
|
|
182
|
+
args = parser.parse_args(argv)
|
|
183
|
+
try:
|
|
184
|
+
return int(args.func(args))
|
|
185
|
+
except KeyboardInterrupt:
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
sys.exit(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CLN type facade."""
|
|
2
|
+
from .types_mesh import ClnConduitType, ClnGwConnection, ClnMesh, ClnNode
|
|
3
|
+
from .types_params import AutoOr, ClnConduitParams, ClnWellParams
|
|
4
|
+
from .types_source import ClnImportedData, ClnPackage, ClnSource, IMPORTED_CLN_SOURCE_ID
|
|
5
|
+
from .types_targets import ClnBCTarget, ResolvedClnRecord, ResolvedClnTarget, SelectionMode
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AutoOr", "ClnWellParams", "ClnConduitParams", "ClnSource",
|
|
9
|
+
"ClnPackage", "ClnImportedData", "ClnNode", "ClnGwConnection",
|
|
10
|
+
"ClnConduitType", "ClnMesh", "ClnBCTarget", "ResolvedClnRecord",
|
|
11
|
+
"ResolvedClnTarget", "SelectionMode", "IMPORTED_CLN_SOURCE_ID",
|
|
12
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""CLN mesh output models."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Literal, Optional, Union
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
ClnRawScalar = Union[int, float]
|
|
8
|
+
|
|
9
|
+
class ClnNode(BaseModel):
|
|
10
|
+
"""A single CLN node — one entry in the ``MfUsgCln`` ``node_prop`` table.
|
|
11
|
+
|
|
12
|
+
Example: a vertical well partially penetrating layers 0-2 produces
|
|
13
|
+
three :class:`ClnNode` entries (one per layer it intersects),
|
|
14
|
+
each with ``ifdir=0``, ``fangle=π/2``, and a ``fleng`` equal to
|
|
15
|
+
the screen length within that layer.
|
|
16
|
+
|
|
17
|
+
Attributes
|
|
18
|
+
----------
|
|
19
|
+
index : int
|
|
20
|
+
1-based global IFNO (matches FloPy's ``MfUsgCln`` contract).
|
|
21
|
+
source_id : str
|
|
22
|
+
Which :class:`ClnSource` produced this node.
|
|
23
|
+
feature_index : int
|
|
24
|
+
0-based index of the originating feature within the source.
|
|
25
|
+
layer : int
|
|
26
|
+
0-based GWF layer hosting this node.
|
|
27
|
+
x, y : float
|
|
28
|
+
Horizontal coordinates in the model CRS.
|
|
29
|
+
z_top, z_bot : float
|
|
30
|
+
Top and bottom of the node's screen segment.
|
|
31
|
+
fleng : float
|
|
32
|
+
Screen length within this layer.
|
|
33
|
+
fangle : float
|
|
34
|
+
Inclination angle (rad). ``π/2`` for vertical wells.
|
|
35
|
+
ifdir : int
|
|
36
|
+
Direction flag — ``0=vertical``, ``1=horizontal``,
|
|
37
|
+
``2=angled``.
|
|
38
|
+
iflin : int
|
|
39
|
+
Flow regime — ``0=laminar`` (default), ``-1=non-laminar``.
|
|
40
|
+
conduit_type : int
|
|
41
|
+
1-based index into :attr:`ClnMesh.conduit_types`.
|
|
42
|
+
gwf_cell_id : int
|
|
43
|
+
0-based host cell within its layer.
|
|
44
|
+
is_terminal, is_junction : bool
|
|
45
|
+
Network classification flags.
|
|
46
|
+
resolved_strt : float
|
|
47
|
+
Per-node starting head (defaults to ``z_bot`` / FELEV).
|
|
48
|
+
resolved_fskin : float
|
|
49
|
+
Per-node skin factor.
|
|
50
|
+
"""
|
|
51
|
+
index: int
|
|
52
|
+
source_id: str
|
|
53
|
+
feature_index: int
|
|
54
|
+
layer: int
|
|
55
|
+
x: float
|
|
56
|
+
y: float
|
|
57
|
+
z_top: float
|
|
58
|
+
z_bot: float
|
|
59
|
+
fleng: float
|
|
60
|
+
fangle: float
|
|
61
|
+
ifdir: Literal[0, 1, 2]
|
|
62
|
+
iflin: Literal[-1, 0] = 0
|
|
63
|
+
conduit_type: int
|
|
64
|
+
gwf_cell_id: int
|
|
65
|
+
is_terminal: bool = False
|
|
66
|
+
is_junction: bool = False
|
|
67
|
+
resolved_strt: float
|
|
68
|
+
resolved_fskin: float
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ClnGwConnection(BaseModel):
|
|
72
|
+
"""One CLN-GWF coupling record in the ``cln_gwc`` table.
|
|
73
|
+
|
|
74
|
+
Each :class:`ClnNode` produces one connection per GWF cell it
|
|
75
|
+
intersects. The Thiem equation parameters live here, not on the
|
|
76
|
+
node itself, so a single node can couple to multiple cells with
|
|
77
|
+
different ``flengw`` and ``faniso`` values.
|
|
78
|
+
|
|
79
|
+
Attributes
|
|
80
|
+
----------
|
|
81
|
+
cln_node_index : int
|
|
82
|
+
1-based, matches :attr:`ClnNode.index`.
|
|
83
|
+
gwf_node_index : int
|
|
84
|
+
1-based GWF node id, computed as
|
|
85
|
+
``layer * ncpl + cell_id + 1``.
|
|
86
|
+
connection_type : int
|
|
87
|
+
``0=Thiem``, ``1=Thiem+skin``. Default ``0``.
|
|
88
|
+
fskin : float
|
|
89
|
+
Skin factor (only consulted when
|
|
90
|
+
``connection_type=1``). Default ``0.0``.
|
|
91
|
+
flengw : float
|
|
92
|
+
Wetted length (m).
|
|
93
|
+
faniso : float
|
|
94
|
+
Anisotropy factor for the surrounding cell. Default ``1.0``.
|
|
95
|
+
icgwadi : int
|
|
96
|
+
Adaptive-flow flag. Default ``0``.
|
|
97
|
+
"""
|
|
98
|
+
cln_node_index: int
|
|
99
|
+
gwf_node_index: int
|
|
100
|
+
connection_type: int = 0
|
|
101
|
+
fskin: float = 0.0
|
|
102
|
+
flengw: float
|
|
103
|
+
faniso: float = 1.0
|
|
104
|
+
icgwadi: int = 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ClnConduitType(BaseModel):
|
|
108
|
+
"""Deduplicated conduit type table entry — one ``cln_circ`` row.
|
|
109
|
+
|
|
110
|
+
Identical (frad, conduitk) pairs are deduplicated so the
|
|
111
|
+
``cln_circ`` table stays compact even when hundreds of nodes
|
|
112
|
+
share the same screen geometry.
|
|
113
|
+
|
|
114
|
+
Attributes
|
|
115
|
+
----------
|
|
116
|
+
type_index : int
|
|
117
|
+
1-based row index.
|
|
118
|
+
frad : float
|
|
119
|
+
Effective radius (m).
|
|
120
|
+
conduitk : float
|
|
121
|
+
Conduit conductivity.
|
|
122
|
+
"""
|
|
123
|
+
type_index: int
|
|
124
|
+
frad: float
|
|
125
|
+
conduitk: float
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ClnMesh(BaseModel):
|
|
129
|
+
"""Full CLN mesh ready for the FloPy adapter.
|
|
130
|
+
|
|
131
|
+
Example: a model with two well sources and one conduit source
|
|
132
|
+
produces a :class:`ClnMesh` whose ``len(nodes)`` is the total
|
|
133
|
+
node count, whose ``conduit_types`` deduplicates the screen
|
|
134
|
+
geometries, and whose ``nndcln`` records one entry per segment
|
|
135
|
+
(well or conduit).
|
|
136
|
+
|
|
137
|
+
Attributes
|
|
138
|
+
----------
|
|
139
|
+
nodes : list[ClnNode]
|
|
140
|
+
Every CLN node, in IFNO order.
|
|
141
|
+
connections : list[ClnGwConnection]
|
|
142
|
+
Every CLN-GWF connection record.
|
|
143
|
+
conduit_types : list[ClnConduitType]
|
|
144
|
+
The deduplicated conduit type table.
|
|
145
|
+
nndcln : list[int]
|
|
146
|
+
Per-segment node counts, in segment order.
|
|
147
|
+
is_network : bool
|
|
148
|
+
Whether junction detection ran. Default ``False``.
|
|
149
|
+
clncon : list[list[int]], optional
|
|
150
|
+
Network connectivity table when :attr:`is_network` is ``True``.
|
|
151
|
+
"""
|
|
152
|
+
nodes: list[ClnNode] = Field(default_factory=list)
|
|
153
|
+
connections: list[ClnGwConnection] = Field(default_factory=list)
|
|
154
|
+
conduit_types: list[ClnConduitType] = Field(default_factory=list)
|
|
155
|
+
nndcln: list[int] = Field(default_factory=list)
|
|
156
|
+
is_network: bool = False
|
|
157
|
+
clncon: Optional[list[list[int]]] = None
|
|
158
|
+
ncln: int = 0
|
|
159
|
+
nclnnds: int = 0
|
|
160
|
+
nja_cln: Optional[int] = None
|
|
161
|
+
iac_cln: Optional[list[int]] = None
|
|
162
|
+
ja_cln: Optional[list[int]] = None
|
|
163
|
+
node_prop: list[list[ClnRawScalar]] = Field(default_factory=list)
|
|
164
|
+
nclngwc: int = 0
|
|
165
|
+
cln_gwc: list[list[ClnRawScalar]] = Field(default_factory=list)
|
|
166
|
+
nconduityp: int = 1
|
|
167
|
+
cln_circ: list[list[ClnRawScalar]] = Field(default_factory=list)
|
|
168
|
+
ibound: list[int] = Field(default_factory=list)
|
|
169
|
+
strt: list[float] = Field(default_factory=list)
|