pyskby 0.2.1__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.
pyskby-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyskby
3
+ Version: 0.2.1
4
+ Summary: SKBY spatially and temporally variable source-area hydrological model (Python port of STVSRM)
5
+ Author: wly
6
+ License: MIT
7
+ Keywords: hydrology,rainfall-runoff,watershed,runoff,source-area,infiltration,channel-routing,STVSRM,SKBY
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Hydrology
17
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: numpy
21
+
22
+ # pyskby
23
+
24
+ SKBY(时空变源混合产流)水文模型的 Python 版 —— 由 Java 版 STVSRM 完整移植,
25
+ 与 Java 参考模型**逐位对齐**(对拍通过),支持物理-守恒双模式、多外部水源接入。
26
+
27
+ - **发布名**:`pyskby`
28
+ - **导入名**:`pyskby`
29
+ - **版本**:`0.2.0`
30
+ - **Python**:≥ 3.10,依赖 `numpy`
31
+
32
+ ---
33
+
34
+ ## 1. 安装指令
35
+
36
+ ### 方式一:从 PyPI 安装(发布后)
37
+
38
+ ```bash
39
+ pip install pyskby
40
+ ```
41
+
42
+ ### 方式二:从源码安装(开发模式)
43
+
44
+ ```bash
45
+ cd skby_py
46
+ pip install -e .
47
+ ```
48
+
49
+ ### 验证安装
50
+
51
+ ```bash
52
+ python -c "import pyskby; print(pyskby.__version__)"
53
+ # 输出: 0.2.0
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 2. 调用指令
59
+
60
+ ### 2.1 Python API
61
+
62
+ ```python
63
+ from pyskby.model import SKBYModel
64
+
65
+ # Faithful 模式(默认,与 Java 逐位对齐)
66
+ m = SKBYModel(
67
+ "data/liulan_fupan/params-testnew.csv", # 参数文件
68
+ "data/liulan_fupan/data-test.csv", # 驱动/时序数据
69
+ conservative=False,
70
+ )
71
+ m.run()
72
+ m.write_outlet_csv("outlet.csv") # 流域出口流量
73
+ m.write_qj_csv("QJ.csv") # 逐河道出流明细
74
+
75
+ # Conservative 守恒模式(修复运动波质量损失,≠ Java 基线)
76
+ m2 = SKBYModel("params.csv", "data.csv", conservative=True)
77
+ m2.run()
78
+ ```
79
+
80
+ ### 2.2 命令行
81
+
82
+ pyskby 提供两种命令行入口,底层都调用同一套逻辑(`pyskby.cli:main`)。
83
+
84
+ **方式 A:安装后的 console script(推荐给使用者)**
85
+
86
+ ```bash
87
+ pip install pyskby # 安装后自动注册 skby-run 命令
88
+ skby-run liulan_fupan # faithful 模式(Java 参考/不守恒)
89
+ skby-run liulan_fupan --conservative # 守恒修复版
90
+ ```
91
+
92
+ `skby-run` 默认从**当前工作目录**查找 `sim/<sim>.groovy` 与 `data/`,因此使用者
93
+ 在自己的算例目录(含 `sim/`、`data/`)下运行即可;也可用 `--root PATH` 指定根目录:
94
+
95
+ ```bash
96
+ cd my_case_study && skby-run liulan_fupan --root .
97
+ ```
98
+
99
+ 输出写到 `<root>/output/<sim>/out/outlet_py.csv` 与 `QJ_py.csv`。
100
+
101
+ **打包内置示例(安装后直接跑通)**
102
+
103
+ pyskby 在 wheel 中内置了示例算例 `liulan_fupan`。安装后用 `skby-example` 把示例
104
+ 解包到当前目录,再用 `skby-run` 运行:
105
+
106
+ ```bash
107
+ pip install pyskby
108
+ skby-example liulan_fupan # 复制 ./examples/{sim,data}/liulan_fupan
109
+ skby-run liulan_fupan --root examples # faithful 模式
110
+ skby-run liulan_fupan --root examples --conservative # 守恒模式
111
+ # 结果: ./examples/output/liulan_fupan/out/outlet_py.csv 与 QJ_py.csv
112
+ ```
113
+
114
+ `skby-example` 不带参数则复制全部示例;已存在时跳过(加 `--force` 覆盖)。
115
+
116
+ > **关于 `--root` 与 `site-packages`**
117
+ > 安装后示例其实就在 `site-packages/pyskby/examples/`。`--root` 直接指向它
118
+ > (如 `--root $(python -c "import pyskby,os;print(os.path.dirname(pyskby.__file__))")/examples`)
119
+ > **也能算**,但输出会写到该安装目录下的 `.../examples/output/<sim>/out/`。不推荐:
120
+ > 会污染 pip 管理的安装目录(重装/升级会被清掉),且在无写权限环境下会报 `PermissionError`。
121
+ > 标准做法仍是 `skby-example` 把示例导出到你自己的工作目录再跑。
122
+
123
+ **方式 B:仓库内调试(`run.py`,加载本地源码)**
124
+
125
+ ```bash
126
+ cd skby_py
127
+ python run.py liulan_fupan # 等价于 --root 指向仓库根
128
+ python run.py liulan_fupan --conservative
129
+ ```
130
+
131
+ `run.py` 会把自身所在目录(`skby_py`)插入 `sys.path` 最前,**强制优先加载本地
132
+ `pyskby` 源码**(而非已安装的 PyPI 副本)。因此调试时改动 `pyskby/` 下的代码,
133
+ `python run.py` 立即生效,不会误加载已 `pip install` 的版本。
134
+
135
+ 也可以用模块方式调用(等价于安装后的 console script):
136
+
137
+ ```bash
138
+ python -m pyskby.cli liulan_fupan --conservative --root /path/to/case
139
+ ```
140
+
141
+ ### 2.3 可选水量平衡诊断
142
+
143
+ ```python
144
+ m.run(diag="diag.csv") # 落盘逐时步水量平衡
145
+ ```
146
+
147
+ > 详细参数说明见仓库内 `skby_py/使用手册.md`(产汇流参数、水源/分洪/水库出流等)。
148
+
149
+ ---
150
+
151
+ ## 3. 打包发布指令
152
+
153
+ ### 3.1 一次性安装发布工具
154
+
155
+ ```bash
156
+ pip install --upgrade build twine
157
+ ```
158
+
159
+ ### 3.2 构建分发包
160
+
161
+ ```bash
162
+ cd skby_py
163
+ python -m build
164
+ ```
165
+
166
+ 会生成 `dist/pyskby-0.2.0-py3-none-any.whl`(wheel)和
167
+ `dist/pyskby-0.2.0.tar.gz`(sdist)。
168
+
169
+ ### 3.3 发布到 PyPI
170
+
171
+ ```bash
172
+ # 方式一:只发布 wheel(不含 sdist 源码包,推荐)
173
+ twine upload dist/*.whl
174
+
175
+ # 方式二:同时发布 wheel + sdist
176
+ twine upload dist/*
177
+
178
+ # 先上传到测试服务器验证
179
+ twine upload --repository testpypi dist/*
180
+ ```
181
+
182
+ > `twine upload` 会提示输入 PyPI 账号的 API Token(用户名填 `__token__`,密码填 token)。
183
+
184
+ ### 3.4 发布后验证
185
+
186
+ ```bash
187
+ pip install pyskby
188
+ python -c "import pyskby; print(pyskby.__version__)"
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 4. 升级发布版本
194
+
195
+ 改版本号后需同步两处:
196
+
197
+ 1. `skby_py/pyproject.toml` → `version = "x.y.z"`
198
+ 2. `skby_py/pyskby/__init__.py` → `__version__ = "x.y.z"`
199
+
200
+ 然后重新执行 §3.2 构建、§3.3 上传。
201
+
202
+ ---
203
+
204
+ ## 5. 常见问题
205
+
206
+ - **发布名 vs 导入名**:PyPI 包名和 Python 导入名均为 `pyskby`(`import pyskby`)。
207
+
208
+ - **调试时改了 `pyskby/` 源码,`run.py` 用的还是旧代码?**
209
+ 不会。`run.py` 在导入前把自身目录(`skby_py`)插到 `sys.path` 最前,因此
210
+ `python run.py ...` 永远加载**本地** `pyskby` 源码,即使系统里已 `pip install`
211
+ 过正式版也优先本地。发布后的使用者用 `skby-run` 命令则加载其安装的副本。
212
+
213
+ - **`skby-run` 与 `python run.py` 的区别?**
214
+ 二者调用同一套逻辑(`pyskby.cli:main`)。`skby-run` 是给**安装者**用的
215
+ console script(从 cwd 找算例);`run.py` 是给**开发者**调试用的入口
216
+ (默认把仓库根作为算例根,并强制加载本地源码)。
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyskby"
7
+ version = "0.2.1"
8
+ description = "SKBY spatially and temporally variable source-area hydrological model (Python port of STVSRM)"
9
+ readme = "readme.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "wly" },
14
+ ]
15
+ keywords = [
16
+ "hydrology",
17
+ "rainfall-runoff",
18
+ "watershed",
19
+ "runoff",
20
+ "source-area",
21
+ "infiltration",
22
+ "channel-routing",
23
+ "STVSRM",
24
+ "SKBY",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Science/Research",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Scientific/Engineering :: Hydrology",
36
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
37
+ ]
38
+ dependencies = [
39
+ "numpy",
40
+ ]
41
+
42
+ # Console entry points: `pip install pyskby` -> `skby-run` + `skby-example`
43
+ [project.scripts]
44
+ skby-run = "pyskby.cli:main"
45
+ skby-example = "pyskby.cli:copy_example"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["."]
49
+ include = ["pyskby*"]
50
+
51
+ # 随包分发的数据/文档文件(示例算例需打进 wheel,供 skby-example 解包)
52
+ [tool.setuptools.package-data]
53
+ pyskby = [
54
+ "*.py",
55
+ "examples/**/*",
56
+ ]
57
+
58
+ # --- pytest: run the regression suite from the skby_py root ---
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["autotest"]
61
+ pythonpath = ["."]
62
+
63
+ # --- ruff: lint + format (mirrors USGS pywatershed conventions) ---
64
+ [tool.ruff]
65
+ line-length = 100
66
+ target-version = "py310"
67
+ src = ["pyskby", "autotest"]
68
+
69
+ [tool.ruff.lint]
70
+ select = [
71
+ "E", # pycodestyle errors
72
+ "F", # pyflakes
73
+ "W", # pycodestyle warnings
74
+ "I", # isort
75
+ "B", # flake8-bugbear
76
+ "UP", # pyupgrade
77
+ ]
78
+ ignore = [
79
+ "E501", # line too long (handled by formatter)
80
+ ]
81
+
82
+ [tool.ruff.lint.isort]
83
+ known-first-party = ["pyskby"]
84
+
85
+ [tool.ruff.format]
86
+ quote-style = "double"
@@ -0,0 +1,24 @@
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
+ __version__ = "0.2.0"
20
+
21
+ from . import units
22
+ from .model import SKBYModel
23
+
24
+ __all__ = ["SKBYModel", "units", "__version__"]
@@ -0,0 +1,187 @@
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
+ "--root",
157
+ default=os.getcwd(),
158
+ help="project root containing sim/ and data/ (default: current directory)",
159
+ )
160
+ args = parser.parse_args(argv)
161
+
162
+ root = os.path.abspath(args.root)
163
+ params_file, data_file = parse_groovy(root, args.sim)
164
+ ppath = _resolve(root, args.sim, params_file)
165
+ dpath = _resolve(root, args.sim, data_file)
166
+
167
+ print(f"params: {ppath}")
168
+ print(f"data: {dpath}")
169
+ print(f"mode: {'conservative (守恒修复版)' if args.conservative else 'faithful (Java参考/不守恒)'}")
170
+
171
+ model = SKBYModel(ppath, dpath, conservative=args.conservative)
172
+ model.run()
173
+
174
+ out_dir = os.path.join(root, "output", args.sim, "out")
175
+ os.makedirs(out_dir, exist_ok=True)
176
+ out_path = os.path.join(out_dir, "outlet_py.csv")
177
+ model.write_outlet_csv(out_path)
178
+ print(f"wrote {out_path}")
179
+ qj_path = os.path.join(out_dir, "QJ_py.csv")
180
+ model.write_qj_csv(qj_path)
181
+ print(f"wrote {qj_path}")
182
+ print(f"basin_cfs[0] = {model.basin_cfs_series[0]:.6f}")
183
+ return 0
184
+
185
+
186
+ if __name__ == "__main__":
187
+ sys.exit(main())
@@ -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))