pyskby 0.2.3__py3-none-any.whl
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.
- pyskby/__init__.py +31 -0
- pyskby/cli.py +201 -0
- pyskby/component.py +47 -0
- pyskby/components.py +3417 -0
- pyskby/examples/__init__.py +0 -0
- pyskby/examples/data/liulan_fupan/RemoveEnd.exe +0 -0
- pyskby/examples/data/liulan_fupan/data-test.csv +79 -0
- pyskby/examples/data/liulan_fupan/params-testnew-template.csv +1236 -0
- pyskby/examples/data/liulan_fupan/params-testnew.csv +1236 -0
- pyskby/examples/output/liulan_fupan/out/QJ_py.csv +78 -0
- pyskby/examples/output/liulan_fupan/out/QJ_py_implicit.csv +78 -0
- pyskby/examples/output/liulan_fupan/out/outlet_py.csv +78 -0
- pyskby/examples/output/liulan_fupan/out/outlet_py_implicit.csv +78 -0
- pyskby/examples/sim/liulan_fupan.groovy +40 -0
- pyskby/examples/sim/liulan_fupan_opt.groovy +117 -0
- pyskby/io.py +91 -0
- pyskby/io_oms.py +122 -0
- pyskby/model.py +422 -0
- pyskby/params.py +159 -0
- pyskby/units.py +41 -0
- pyskby-0.2.3.dist-info/METADATA +246 -0
- pyskby-0.2.3.dist-info/RECORD +25 -0
- pyskby-0.2.3.dist-info/WHEEL +5 -0
- pyskby-0.2.3.dist-info/entry_points.txt +3 -0
- pyskby-0.2.3.dist-info/top_level.txt +1 -0
pyskby/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""pyskby — SKBY (spatially and temporally variable source-area) hydrological model.
|
|
2
|
+
|
|
3
|
+
Pure-Python (numpy/pandas) port of STVSRM_old. The import name is ``pyskby``,
|
|
4
|
+
matching the PyPI package name.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from pyskby.model import SKBYModel
|
|
9
|
+
m = SKBYModel(params_csv, data_csv)
|
|
10
|
+
m.run()
|
|
11
|
+
m.write_outlet_csv("outlet.csv")
|
|
12
|
+
|
|
13
|
+
All units follow the original OMS3/PRMS conventions (inches/feet/cms), with
|
|
14
|
+
conversion constants centralized in ``pyskby.units``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__author__ = "wly"
|
|
18
|
+
|
|
19
|
+
# 版本号单一来源:以 pyproject.toml [project].version 为准,
|
|
20
|
+
# 通过包元数据动态读取,避免两处维护不一致。
|
|
21
|
+
try:
|
|
22
|
+
from importlib.metadata import version as _version
|
|
23
|
+
|
|
24
|
+
__version__ = _version("pyskby")
|
|
25
|
+
except Exception: # 兜底(如 editable 安装未完成时)
|
|
26
|
+
__version__ = "0.2.3"
|
|
27
|
+
|
|
28
|
+
from . import units
|
|
29
|
+
from .model import SKBYModel
|
|
30
|
+
|
|
31
|
+
__all__ = ["SKBYModel", "units", "__version__"]
|
pyskby/cli.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Command-line entry points for the pyskby model.
|
|
2
|
+
|
|
3
|
+
Two console scripts are installed with the package:
|
|
4
|
+
|
|
5
|
+
* ``skby-run <sim> [--conservative] [--root PATH]`` — run a simulation case.
|
|
6
|
+
Reads ``sim/<sim>.groovy`` to learn which params/data CSVs to use, runs the
|
|
7
|
+
model, and writes ``output/<sim>/out/outlet_py.csv`` (plus ``QJ_py.csv``).
|
|
8
|
+
|
|
9
|
+
* ``skby-example [name]`` — copy the bundled example case(s) into the current
|
|
10
|
+
working directory (``examples/<sim>``) so they can be run with ``skby-run``.
|
|
11
|
+
|
|
12
|
+
Both are also runnable as a module::
|
|
13
|
+
|
|
14
|
+
python -m pyskby.cli liulan_fupan
|
|
15
|
+
python -m pyskby.cli --example liulan_fupan
|
|
16
|
+
|
|
17
|
+
Mimics ``java -jar RunModel.jar <sim_name> OFF``. The ``<sim_name>`` is resolved
|
|
18
|
+
against a project root, which defaults to the current working directory (so users
|
|
19
|
+
run the CLI from their own case-study folder that contains ``sim/`` and ``data/``).
|
|
20
|
+
Override with ``--root``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__author__ = "wly"
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import importlib.resources as _resources
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import shutil
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from .model import SKBYModel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _examples_dir() -> Path:
|
|
37
|
+
"""Filesystem path to the packaged example cases (works for both the source
|
|
38
|
+
tree and an installed wheel via importlib.resources)."""
|
|
39
|
+
return Path(str(_resources.files("pyskby").joinpath("examples")))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def copy_example(argv=None):
|
|
43
|
+
"""Console-script entry: ``skby-example [name]``.
|
|
44
|
+
|
|
45
|
+
Copies the bundled example(s) into ``./examples/<sim>`` of the current
|
|
46
|
+
working directory. With no ``name`` all examples are copied.
|
|
47
|
+
"""
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="skby-example",
|
|
50
|
+
description="Copy bundled pyskby example case(s) into ./examples/.",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"name",
|
|
54
|
+
nargs="?",
|
|
55
|
+
default=None,
|
|
56
|
+
help="example case name (defaults to all); e.g. liulan_fupan",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--force",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="overwrite an existing ./examples/<name> instead of skipping",
|
|
62
|
+
)
|
|
63
|
+
args = parser.parse_args(argv)
|
|
64
|
+
|
|
65
|
+
src_root = _examples_dir()
|
|
66
|
+
if not src_root.is_dir():
|
|
67
|
+
print(f"error: no bundled examples found at {src_root}", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
sim_dir = src_root.joinpath("sim")
|
|
71
|
+
data_dir = src_root.joinpath("data")
|
|
72
|
+
|
|
73
|
+
def _copy_one(sim_name):
|
|
74
|
+
# Preserve the bundled layout: examples/sim/<name>.groovy and
|
|
75
|
+
# examples/data/<name>/..., so `skby-run <name> --root examples` works.
|
|
76
|
+
dst_sim = os.path.join(os.getcwd(), "examples", "sim")
|
|
77
|
+
dst_data = os.path.join(os.getcwd(), "examples", "data", sim_name)
|
|
78
|
+
gsrc = sim_dir.joinpath(f"{sim_name}.groovy")
|
|
79
|
+
dsrc = data_dir.joinpath(sim_name)
|
|
80
|
+
if (not gsrc.is_file()) and (not dsrc.is_dir()):
|
|
81
|
+
print(f"warning: example '{sim_name}' not found", file=sys.stderr)
|
|
82
|
+
return
|
|
83
|
+
if gsrc.is_file():
|
|
84
|
+
if os.path.exists(os.path.join(dst_sim, f"{sim_name}.groovy")) and not args.force:
|
|
85
|
+
print(f"skip examples/sim/{sim_name}.groovy (exists; --force to overwrite)")
|
|
86
|
+
else:
|
|
87
|
+
os.makedirs(dst_sim, exist_ok=True)
|
|
88
|
+
shutil.copy2(gsrc, os.path.join(dst_sim, f"{sim_name}.groovy"))
|
|
89
|
+
if dsrc.is_dir():
|
|
90
|
+
if os.path.exists(dst_data) and not args.force:
|
|
91
|
+
print(f"skip examples/data/{sim_name} (exists; --force to overwrite)")
|
|
92
|
+
else:
|
|
93
|
+
shutil.copytree(dsrc, dst_data, dirs_exist_ok=True)
|
|
94
|
+
print(f"copied example '{sim_name}' -> ./examples/")
|
|
95
|
+
|
|
96
|
+
if args.name:
|
|
97
|
+
_copy_one(args.name)
|
|
98
|
+
else:
|
|
99
|
+
names = sorted(
|
|
100
|
+
p.stem for p in sim_dir.glob("*.groovy") if p.is_file()
|
|
101
|
+
)
|
|
102
|
+
if not names:
|
|
103
|
+
print("no example cases found", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
for n in names:
|
|
106
|
+
_copy_one(n)
|
|
107
|
+
print("done. Now run: skby-run <name> --root examples")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_groovy(root, sim_name):
|
|
113
|
+
"""Read sim/<sim>.groovy to find the params/data CSV filenames it references."""
|
|
114
|
+
gpath = os.path.join(root, "sim", f"{sim_name}.groovy")
|
|
115
|
+
if not os.path.exists(gpath):
|
|
116
|
+
raise FileNotFoundError(f"sim file not found: {gpath}")
|
|
117
|
+
params_file = "params-testnew.csv"
|
|
118
|
+
data_file = "data-test.csv"
|
|
119
|
+
with open(gpath, "r", encoding="utf-8") as f:
|
|
120
|
+
txt = f.read()
|
|
121
|
+
m = re.search(r'params.*?"([^"]+\.csv)"', txt)
|
|
122
|
+
if m:
|
|
123
|
+
params_file = m.group(1)
|
|
124
|
+
m = re.search(r'data.*?"([^"]+\.csv)"', txt)
|
|
125
|
+
if m:
|
|
126
|
+
data_file = m.group(1)
|
|
127
|
+
return params_file, data_file
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _resolve(root, sim, filename):
|
|
131
|
+
"""Resolve a params/data path: absolute, root-relative, or data/<sim>/-relative."""
|
|
132
|
+
if os.path.isabs(filename):
|
|
133
|
+
return filename
|
|
134
|
+
p = os.path.join(root, filename)
|
|
135
|
+
if os.path.exists(p):
|
|
136
|
+
return p
|
|
137
|
+
alt = os.path.join(root, "data", sim, os.path.basename(filename))
|
|
138
|
+
if os.path.exists(alt):
|
|
139
|
+
return alt
|
|
140
|
+
return p # fall back to the root-relative guess (will surface a clear error)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main(argv=None):
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
prog="skby-run",
|
|
146
|
+
description="Run the pyskby hydrological model for a given simulation case.",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("sim", help="simulation name (matches sim/<sim>.groovy)")
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--conservative",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="mass-conserving routing (Newton-implicit non-linear reservoirs); "
|
|
153
|
+
"default is faithful (Java-reference behaviour, not water-conserving).",
|
|
154
|
+
)
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--routing",
|
|
157
|
+
choices=["explicit", "implicit"],
|
|
158
|
+
default="explicit",
|
|
159
|
+
help="kinematic-wave scheme: 'explicit' (default, faithful Java) or "
|
|
160
|
+
"'implicit' (opt-in 4-point implicit scheme, for an explicit-vs-implicit "
|
|
161
|
+
"comparison on the same case).",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--root",
|
|
165
|
+
default=os.getcwd(),
|
|
166
|
+
help="project root containing sim/ and data/ (default: current directory)",
|
|
167
|
+
)
|
|
168
|
+
args = parser.parse_args(argv)
|
|
169
|
+
|
|
170
|
+
root = os.path.abspath(args.root)
|
|
171
|
+
params_file, data_file = parse_groovy(root, args.sim)
|
|
172
|
+
ppath = _resolve(root, args.sim, params_file)
|
|
173
|
+
dpath = _resolve(root, args.sim, data_file)
|
|
174
|
+
|
|
175
|
+
print(f"params: {ppath}")
|
|
176
|
+
print(f"data: {dpath}")
|
|
177
|
+
print(f"mode: {'conservative (守恒修复版)' if args.conservative else 'faithful (Java参考/不守恒)'}"
|
|
178
|
+
f" | routing: {args.routing}")
|
|
179
|
+
|
|
180
|
+
model = SKBYModel(ppath, dpath, conservative=args.conservative,
|
|
181
|
+
routing=args.routing)
|
|
182
|
+
model.run()
|
|
183
|
+
|
|
184
|
+
out_dir = os.path.join(root, "output", args.sim, "out")
|
|
185
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
186
|
+
# Keep faithful and conservative outputs separate so a conservative run
|
|
187
|
+
# never overwrites the faithful (Java-reference) results.
|
|
188
|
+
suffix = ("_conservative" if args.conservative else "") + (
|
|
189
|
+
"_implicit" if args.routing == "implicit" else "")
|
|
190
|
+
out_path = os.path.join(out_dir, f"outlet_py{suffix}.csv")
|
|
191
|
+
model.write_outlet_csv(out_path)
|
|
192
|
+
print(f"wrote {out_path}")
|
|
193
|
+
qj_path = os.path.join(out_dir, f"QJ_py{suffix}.csv")
|
|
194
|
+
model.write_qj_csv(qj_path)
|
|
195
|
+
print(f"wrote {qj_path}")
|
|
196
|
+
print(f"basin_cfs[0] = {model.basin_cfs_series[0]:.6f}")
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
pyskby/component.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Component interface contract (phase-2 pluggable-process design).
|
|
2
|
+
|
|
3
|
+
Per doc/升级改造说明.md §4.3, the model components will become interchangeable
|
|
4
|
+
so that algorithms (e.g. infiltration, PET) can be swapped without disturbing the
|
|
5
|
+
overall assembly -- mirroring USGS pywatershed's "replaceable conceptualization".
|
|
6
|
+
|
|
7
|
+
This module defines the shared contract. Existing components (components.py) are
|
|
8
|
+
NOT yet required to inherit from it; this is the intended target interface and a
|
|
9
|
+
safe place to evolve toward. New/refactored components should subclass
|
|
10
|
+
``BaseComponent`` and implement ``initialize``/``run``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__author__ = "wly"
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseComponent(ABC):
|
|
21
|
+
"""Common interface for every model process component.
|
|
22
|
+
|
|
23
|
+
Subclasses receive the parsed parameter object in ``__init__`` and expose a
|
|
24
|
+
``run`` method that advances one time step. State is held on the instance so
|
|
25
|
+
that multi-step runs preserve cross-step memory (e.g. channel/groundwater
|
|
26
|
+
states), matching the Java per-component state model.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
#: logical name used in diagnostics / registry (e.g. "HRUOut")
|
|
30
|
+
name: str = "component"
|
|
31
|
+
|
|
32
|
+
def __init__(self, cfg):
|
|
33
|
+
self.cfg = cfg
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def initialize(self) -> None:
|
|
37
|
+
"""One-time setup before the simulation loop (allocate state arrays)."""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def run(self, *args, **kwargs) -> object:
|
|
42
|
+
"""Advance one time step; return the component's contribution(s)."""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def nhru(self) -> int:
|
|
47
|
+
return int(self.cfg.get("nhru", 1))
|