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