bpaflow 0.1.0__py3-none-win_amd64.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.
- bpaflow/__init__.py +3546 -0
- bpaflow/_native/README.md +5 -0
- bpaflow/_native/THIRD_PARTY_NOTICES.md +26 -0
- bpaflow/_native/libgcc_s_seh-1.dll +0 -0
- bpaflow/_native/libgfortran-5.dll +0 -0
- bpaflow/_native/libipfpy.dll +0 -0
- bpaflow/_native/libquadmath-0.dll +0 -0
- bpaflow/_native/libwinpthread-1.dll +0 -0
- bpaflow/actions.py +450 -0
- bpaflow/agent.py +744 -0
- bpaflow/cards.py +582 -0
- bpaflow/protocol.py +103 -0
- bpaflow/remote.py +135 -0
- bpaflow/server.py +249 -0
- bpaflow-0.1.0.dist-info/METADATA +280 -0
- bpaflow-0.1.0.dist-info/RECORD +19 -0
- bpaflow-0.1.0.dist-info/WHEEL +5 -0
- bpaflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- bpaflow-0.1.0.dist-info/top_level.txt +1 -0
bpaflow/__init__.py
ADDED
|
@@ -0,0 +1,3546 @@
|
|
|
1
|
+
# bpaflow: Python 直调 libipfpy 的潮流 API 包(ctypes,无第三方硬依赖)。
|
|
2
|
+
#
|
|
3
|
+
# 与 pyflow(socket 客户端)并存;本包在同一进程内以 DLL 直调方式驱动
|
|
4
|
+
# 潮流求解内核,并额外暴露内部计算数据:
|
|
5
|
+
# - y_matrix(): 导纳矩阵(稀疏 CSR,G/B 分列)
|
|
6
|
+
# - jacobian(): 因子化雅可比(/amtrx/ 布局,含行指针 jndex)
|
|
7
|
+
# - mismatch(): 求解后偏差汇总(P/Q/总/角度 与迭代计数)
|
|
8
|
+
# - iteration_history(): 每次迭代误差最大的 8 条母线(/errbus/)
|
|
9
|
+
# - voltages(): 电压实部/虚部/基准电压及内外序映射
|
|
10
|
+
#
|
|
11
|
+
# 修改类(P0,详见 docs/api/BPA潮流API说明.md):
|
|
12
|
+
# - cards.py: 120 列变更卡构造/解析(列位与引擎源码逐字段对齐)
|
|
13
|
+
# - actions.py: 高层动作(Changeset:set_gen_p/outage_branch/
|
|
14
|
+
# scale_load/...,带快照与读回校验)
|
|
15
|
+
# - put_inrecs(): 批量提交变更卡(逐卡 rc + 引擎日志)
|
|
16
|
+
# - solve_case(): 求解并返回结构化结果(收敛性/偏差/电压/重载/违规)
|
|
17
|
+
# - violations(): 电压越限、支路过载、无功出力受限清单
|
|
18
|
+
# - capture(): 引擎调用期间捕获 fd 1/2 输出;engine_log() 取回
|
|
19
|
+
#
|
|
20
|
+
# 用法:
|
|
21
|
+
# from bpaflow import BPAFlow
|
|
22
|
+
# pf = BPAFlow() # 自动定位 libipfpy.dll,或 BPAFLOW_DLL 环境变量
|
|
23
|
+
# pf.init()
|
|
24
|
+
# pf.load_oldbase("bench.bse")
|
|
25
|
+
# with pf.changes() as cs:
|
|
26
|
+
# cs.set_gen_p("GEN1", 230.0, 150.0)
|
|
27
|
+
# cs.outage_branch("BUS1", 230.0, "BUS2", 230.0, "1")
|
|
28
|
+
# rpt = pf.solve_case(changes=cs)
|
|
29
|
+
# print(rpt["violations"]["counts"])
|
|
30
|
+
# pf.close() # 收尾(关闭引擎文件单元;也可省略)
|
|
31
|
+
#
|
|
32
|
+
# 或用上下文管理器(自动 init,退出时自动 close):
|
|
33
|
+
# with BPAFlow() as pf:
|
|
34
|
+
# pf.load_oldbase("bench.bse")
|
|
35
|
+
# pf.solution()
|
|
36
|
+
"""BPAFlow: 潮流 Python API(直调 libipfpy)。"""
|
|
37
|
+
|
|
38
|
+
import ctypes
|
|
39
|
+
import json
|
|
40
|
+
import math
|
|
41
|
+
import os
|
|
42
|
+
import sys
|
|
43
|
+
import tempfile
|
|
44
|
+
import time
|
|
45
|
+
|
|
46
|
+
from ctypes import (CDLL, c_int, c_float, c_double, c_char, c_char_p,
|
|
47
|
+
byref, create_string_buffer, c_void_p)
|
|
48
|
+
|
|
49
|
+
from . import cards as _cards
|
|
50
|
+
from .actions import Changeset as _Changeset
|
|
51
|
+
from .actions import submit_card as _submit_card
|
|
52
|
+
|
|
53
|
+
__all__ = ["BPAFlow", "PyFlowError", "SolutionRC", "connect", "__version__"]
|
|
54
|
+
|
|
55
|
+
__version__ = "0.1.0"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SolutionRC:
|
|
59
|
+
"""潮流求解返回码语义(pf_solution / pf_solve_area)。
|
|
60
|
+
|
|
61
|
+
用法::
|
|
62
|
+
if rc == 0:
|
|
63
|
+
print("收敛")
|
|
64
|
+
elif rc == SolutionRC.NOT_CONVERGED:
|
|
65
|
+
print("发散")
|
|
66
|
+
|
|
67
|
+
返回码对应 BPA-IPF 引擎 erfp.f 63-71 (±8Δ²ΔбäÁ¿¼ì²é) 及 p_cmde2.f
|
|
68
|
+
退出状态(er_quit 累计 fatal error 计数)。
|
|
69
|
+
"""
|
|
70
|
+
CONVERGED = 0
|
|
71
|
+
NOT_CONVERGED = 7
|
|
72
|
+
ERROR = -1
|
|
73
|
+
_DESC = {
|
|
74
|
+
0: "收敛(CONVERGED)",
|
|
75
|
+
7: "发散(NOT CONVERGED)",
|
|
76
|
+
-1: "引擎错误(ERROR)",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def describe(cls, rc):
|
|
81
|
+
return cls._DESC.get(rc, "未知返回码 %d" % rc)
|
|
82
|
+
|
|
83
|
+
MAXBUS = 13000
|
|
84
|
+
MAXBRN = MAXBUS * 5 // 2
|
|
85
|
+
MAXYE = MAXBRN * 5 // 2
|
|
86
|
+
MAXLTC = MAXBRN // 10
|
|
87
|
+
MAXTIE = MAXBRN // 10
|
|
88
|
+
MAXMTX = 8 * MAXYE
|
|
89
|
+
|
|
90
|
+
# 引擎日志环形缓冲上限(8 MiB,足够覆盖一次完整求解输出)
|
|
91
|
+
_ENGINE_LOG_LIMIT = 8 * 1024 * 1024
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PyFlowError(RuntimeError):
|
|
95
|
+
"""潮流引擎错误。"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _candidate_dll_paths():
|
|
99
|
+
cands = []
|
|
100
|
+
env = os.environ.get("BPAFLOW_DLL")
|
|
101
|
+
if env:
|
|
102
|
+
cands.append(env)
|
|
103
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
104
|
+
roots = [here] + [os.path.dirname(here)]
|
|
105
|
+
for r in roots:
|
|
106
|
+
for sub in ("_native", "", "bin", "_build/pf/lib/cflow", "_build/apps",
|
|
107
|
+
"_build/apps/bin",
|
|
108
|
+
"_build/apps/Release", "_build", "_build/Release",
|
|
109
|
+
"_build/apps/lib/cflow", "_build/apps/lib"):
|
|
110
|
+
cands.append(os.path.join(r, sub, "libipfpy.dll"))
|
|
111
|
+
return cands
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _find_dll():
|
|
115
|
+
for p in _candidate_dll_paths():
|
|
116
|
+
if os.path.isfile(p):
|
|
117
|
+
return p
|
|
118
|
+
raise PyFlowError(
|
|
119
|
+
"libipfpy.dll 未找到;请设置环境变量 BPAFLOW_DLL 指向 DLL 路径。"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _runtime_dirs():
|
|
124
|
+
"""候选的 MinGW 运行库目录(libgfortran-5.dll 等所在目录)。"""
|
|
125
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
126
|
+
dirs = [os.path.join(here, "_native")]
|
|
127
|
+
env = os.environ.get("BPAFLOW_RUNTIME_DIR")
|
|
128
|
+
if env:
|
|
129
|
+
dirs += [d for d in env.split(os.pathsep) if d]
|
|
130
|
+
for d in os.environ.get("PATH", "").split(os.pathsep):
|
|
131
|
+
d = d.strip('"')
|
|
132
|
+
if d and os.path.isfile(os.path.join(d, "libgfortran-5.dll")):
|
|
133
|
+
dirs.append(d)
|
|
134
|
+
return dirs
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_RESOLVED_DLL_PATH = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _gfortran_flush_candidates():
|
|
141
|
+
"""libgfortran-5.dll 的候选路径(刷新 Fortran unit 缓冲用)。"""
|
|
142
|
+
cands = []
|
|
143
|
+
if _RESOLVED_DLL_PATH:
|
|
144
|
+
cands.append(os.path.join(os.path.dirname(_RESOLVED_DLL_PATH),
|
|
145
|
+
"libgfortran-5.dll"))
|
|
146
|
+
for d in _runtime_dirs():
|
|
147
|
+
cands.append(os.path.join(d, "libgfortran-5.dll"))
|
|
148
|
+
# 进程里已由 libipfpy 依赖载入时,按模块名即可解析。
|
|
149
|
+
cands.append("libgfortran-5.dll")
|
|
150
|
+
return cands
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _flush_gfortran_streams(units=(0, 6)):
|
|
154
|
+
"""尽力刷新 libgfortran 预连接单元(0=stderr, 6=stdout)。
|
|
155
|
+
|
|
156
|
+
重定向期间 Fortran 输出停在 libgfortran 的缓冲里,C 运行时的
|
|
157
|
+
fflush 推不动它;必须经 _gfortran_flush_i4 调用。失败静默。
|
|
158
|
+
"""
|
|
159
|
+
import ctypes
|
|
160
|
+
for cand in _gfortran_flush_candidates():
|
|
161
|
+
try:
|
|
162
|
+
lib = ctypes.CDLL(cand)
|
|
163
|
+
except OSError:
|
|
164
|
+
continue
|
|
165
|
+
fn = getattr(lib, "_gfortran_flush_i4", None)
|
|
166
|
+
if fn is None:
|
|
167
|
+
continue
|
|
168
|
+
fn.argtypes = [ctypes.POINTER(ctypes.c_int)]
|
|
169
|
+
fn.restype = None
|
|
170
|
+
for u in units:
|
|
171
|
+
unit = ctypes.c_int(u)
|
|
172
|
+
try:
|
|
173
|
+
fn(ctypes.byref(unit))
|
|
174
|
+
except Exception: # pragma: no cover - 防御性
|
|
175
|
+
pass
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _load_lib(dll_path):
|
|
180
|
+
global _RESOLVED_DLL_PATH
|
|
181
|
+
_RESOLVED_DLL_PATH = os.path.abspath(dll_path)
|
|
182
|
+
if os.name == "nt" and hasattr(os, "add_dll_directory"):
|
|
183
|
+
for d in [os.path.dirname(os.path.abspath(dll_path))] + \
|
|
184
|
+
_runtime_dirs():
|
|
185
|
+
try:
|
|
186
|
+
os.add_dll_directory(d)
|
|
187
|
+
except OSError:
|
|
188
|
+
pass
|
|
189
|
+
return CDLL(dll_path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# 尝试加载 numpy(可选);无 numpy 时退回 ctypes 数组
|
|
193
|
+
try:
|
|
194
|
+
import numpy as _np
|
|
195
|
+
except ImportError:
|
|
196
|
+
_np = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _arr(ct_array, n, dtype=None):
|
|
200
|
+
"""ctypes 数组 -> numpy 数组(无 numpy 时返回 ctypes 数组切片)。"""
|
|
201
|
+
if _np is not None:
|
|
202
|
+
a = _np.frombuffer(ct_array, count=n, dtype=dtype
|
|
203
|
+
if dtype is not None else _np.float64)
|
|
204
|
+
return a.copy()
|
|
205
|
+
return list(ct_array[:n])
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _ascii(s):
|
|
209
|
+
return s.encode("ascii") if isinstance(s, str) else s
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _text(b):
|
|
213
|
+
return bytes(b).split(b"\x00")[0].decode("ascii", "replace")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _norm_name(name):
|
|
217
|
+
return (name or "").strip().upper()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _branch_dedup_key(branch):
|
|
221
|
+
"""方向无关的支路去重键,保留分段号。"""
|
|
222
|
+
endpoints = tuple(sorted([
|
|
223
|
+
(_norm_name(branch.get("bus1_name")), round(float(branch.get("kv1", 0.0)), 2)),
|
|
224
|
+
(_norm_name(branch.get("bus2_name")), round(float(branch.get("kv2", 0.0)), 2)),
|
|
225
|
+
]))
|
|
226
|
+
return (endpoints, (branch.get("ckt") or "").strip(),
|
|
227
|
+
(branch.get("type") or "").strip(),
|
|
228
|
+
int(branch.get("section") or 0))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _branch_kind(btype):
|
|
232
|
+
t = (btype or "").strip().upper()
|
|
233
|
+
if t in ("L", "E"):
|
|
234
|
+
return "line"
|
|
235
|
+
if t in ("T", "TP"):
|
|
236
|
+
return "transformer"
|
|
237
|
+
if t in ("LD", "LM"):
|
|
238
|
+
return "dc_line"
|
|
239
|
+
if t in ("R", "RM", "RN", "RQ", "RV", "RP"):
|
|
240
|
+
return "control"
|
|
241
|
+
return "unknown"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _branch_loading(branch):
|
|
245
|
+
"""按 BPA 输出语义计算支路负载率。
|
|
246
|
+
|
|
247
|
+
L/E 类支路使用安培负载率,T/TP/R 类使用 MVA 负载率;优先采用
|
|
248
|
+
引擎输出中的 tot_*_load_pct,其次采用 crit_* 负载/额定,最后才
|
|
249
|
+
用端子视在功率除以 nominal rating 回退。返回值中的
|
|
250
|
+
loading_ratio 为比值,loading_pct 为百分数。
|
|
251
|
+
"""
|
|
252
|
+
available = bool(branch.get("solution_available", True))
|
|
253
|
+
if not available:
|
|
254
|
+
return {"loading_ratio": None, "loading_pct": None,
|
|
255
|
+
"loading_kind": None, "rating_used": None,
|
|
256
|
+
"rating_code": None, "s_mva": None, "i_amps": None}
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
s_mva = max(math.hypot(float(branch.get("Pin") or 0.0),
|
|
260
|
+
float(branch.get("Qin") or 0.0)),
|
|
261
|
+
math.hypot(float(branch.get("Pout") or 0.0),
|
|
262
|
+
float(branch.get("Qout") or 0.0)))
|
|
263
|
+
except (TypeError, ValueError):
|
|
264
|
+
s_mva = 0.0
|
|
265
|
+
btype = (branch.get("type") or "").strip().upper()
|
|
266
|
+
if btype in ("LD", "LM"):
|
|
267
|
+
return {"loading_ratio": None, "loading_pct": None,
|
|
268
|
+
"loading_kind": "dc_line", "rating_used": None,
|
|
269
|
+
"rating_code": None, "s_mva": None, "i_amps": None}
|
|
270
|
+
if btype in ("R", "RM", "RN", "RQ", "RV", "RP"):
|
|
271
|
+
return {"loading_ratio": None, "loading_pct": None,
|
|
272
|
+
"loading_kind": "control_record", "rating_used": None,
|
|
273
|
+
"rating_code": None, "s_mva": None, "i_amps": None}
|
|
274
|
+
kv1 = float(branch.get("kv1") or 0.0)
|
|
275
|
+
total_rating = float(branch.get("total_rating") or 0.0)
|
|
276
|
+
ratio = None
|
|
277
|
+
kind = None
|
|
278
|
+
rating = None
|
|
279
|
+
code = None
|
|
280
|
+
i_amps = None
|
|
281
|
+
|
|
282
|
+
if btype in ("L", "E"):
|
|
283
|
+
pct = float(branch.get("tot_line_load_pct") or 0.0)
|
|
284
|
+
if pct > 0.0:
|
|
285
|
+
ratio = pct / 100.0
|
|
286
|
+
kind = "line_amps"
|
|
287
|
+
else:
|
|
288
|
+
amps = float(branch.get("crit_line_load_amps") or 0.0)
|
|
289
|
+
rating = float(branch.get("crit_line_rat_amps") or 0.0)
|
|
290
|
+
if rating > 0.0 and amps > 0.0:
|
|
291
|
+
ratio = amps / rating
|
|
292
|
+
kind = "line_amps"
|
|
293
|
+
elif kv1 > 0.0 and total_rating > 0.0:
|
|
294
|
+
i_amps = s_mva * 1000.0 / (math.sqrt(3.0) * kv1)
|
|
295
|
+
ratio = i_amps / total_rating
|
|
296
|
+
kind = "line_amps_fallback"
|
|
297
|
+
rating = total_rating
|
|
298
|
+
if i_amps is None:
|
|
299
|
+
i_amps = float(branch.get("crit_line_load_amps") or 0.0)
|
|
300
|
+
if rating is None:
|
|
301
|
+
rating = float(branch.get("crit_line_rat_amps") or 0.0)
|
|
302
|
+
code = branch.get("crit_line_rat_code") or "N"
|
|
303
|
+
elif btype in ("T", "TP", "R", "RM", "RN", "RQ", "RV", "RP"):
|
|
304
|
+
pct = float(branch.get("tot_xfmr_load_pct") or 0.0)
|
|
305
|
+
if pct > 0.0:
|
|
306
|
+
ratio = pct / 100.0
|
|
307
|
+
kind = "transformer_mva"
|
|
308
|
+
else:
|
|
309
|
+
load = float(branch.get("crit_xfmr_load_mva") or 0.0)
|
|
310
|
+
rating = float(branch.get("crit_xfmr_rat_mva") or 0.0)
|
|
311
|
+
if rating > 0.0 and load > 0.0:
|
|
312
|
+
ratio = load / rating
|
|
313
|
+
kind = "transformer_mva"
|
|
314
|
+
elif total_rating > 0.0:
|
|
315
|
+
ratio = s_mva / total_rating
|
|
316
|
+
kind = "transformer_mva_fallback"
|
|
317
|
+
rating = total_rating
|
|
318
|
+
if rating is None:
|
|
319
|
+
rating = float(branch.get("crit_xfmr_rat_mva") or 0.0)
|
|
320
|
+
code = branch.get("crit_xfmr_rat_code") or "N"
|
|
321
|
+
else:
|
|
322
|
+
kind = "unsupported_branch_type"
|
|
323
|
+
|
|
324
|
+
if ratio is None:
|
|
325
|
+
loading_pct = None
|
|
326
|
+
else:
|
|
327
|
+
loading_pct = ratio * 100.0
|
|
328
|
+
return {"loading_ratio": ratio, "loading_pct": loading_pct,
|
|
329
|
+
"loading_kind": kind, "rating_used": rating,
|
|
330
|
+
"rating_code": code, "s_mva": s_mva, "i_amps": i_amps}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _pfc_filename(command):
|
|
334
|
+
"""Extract FILE= from a PFC command, including quoted paths with spaces."""
|
|
335
|
+
import re
|
|
336
|
+
match = re.search(
|
|
337
|
+
r"FILE\s*=\s*(?:\"([^\"]+)\"|'([^']+)'|([^,\s]+))",
|
|
338
|
+
command, re.IGNORECASE)
|
|
339
|
+
if not match:
|
|
340
|
+
return None
|
|
341
|
+
return next(value for value in match.groups() if value is not None)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class _EngineCapture(object):
|
|
345
|
+
"""引擎调用期间捕获 fd 1/2(gfortran unit 6 直写 stdout)。
|
|
346
|
+
|
|
347
|
+
输出落到临时文件(避免管道阻塞死锁),结束后整段读回追加到 sink
|
|
348
|
+
(bytes 片段列表)。替代旧 batch_n1._quiet 的"丢 NUL"方式。
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def __init__(self, sink):
|
|
352
|
+
self._sink = sink
|
|
353
|
+
|
|
354
|
+
def __enter__(self):
|
|
355
|
+
sys.stdout.flush()
|
|
356
|
+
sys.stderr.flush()
|
|
357
|
+
self._saved_out = os.dup(1)
|
|
358
|
+
self._saved_err = os.dup(2)
|
|
359
|
+
fd, self._path = tempfile.mkstemp(prefix="bpaflow_cap_",
|
|
360
|
+
suffix=".log")
|
|
361
|
+
os.close(fd)
|
|
362
|
+
self._cap = os.open(self._path, os.O_WRONLY | os.O_APPEND)
|
|
363
|
+
os.dup2(self._cap, 1)
|
|
364
|
+
os.dup2(self._cap, 2)
|
|
365
|
+
return self
|
|
366
|
+
|
|
367
|
+
def __exit__(self, exc_type, exc, tb):
|
|
368
|
+
# 重定向期间 C 运行时把 stdout 视为全缓冲,引擎 printf / Fortran
|
|
369
|
+
# unit 6 的尾部滞留缓冲;先刷新二者,再恢复 fd 并读回日志。
|
|
370
|
+
import ctypes
|
|
371
|
+
for _crt in ("msvcrt", "ucrtbase"):
|
|
372
|
+
try:
|
|
373
|
+
ctypes.CDLL(_crt).fflush(None)
|
|
374
|
+
except OSError:
|
|
375
|
+
pass
|
|
376
|
+
_flush_gfortran_streams()
|
|
377
|
+
sys.stdout.flush()
|
|
378
|
+
os.dup2(self._saved_out, 1)
|
|
379
|
+
os.dup2(self._saved_err, 2)
|
|
380
|
+
os.close(self._cap)
|
|
381
|
+
os.close(self._saved_out)
|
|
382
|
+
os.close(self._saved_err)
|
|
383
|
+
try:
|
|
384
|
+
with open(self._path, "rb") as f:
|
|
385
|
+
data = f.read()
|
|
386
|
+
finally:
|
|
387
|
+
try:
|
|
388
|
+
os.unlink(self._path)
|
|
389
|
+
except OSError:
|
|
390
|
+
pass
|
|
391
|
+
if data:
|
|
392
|
+
self._sink.append(data)
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class BPAFlow(object):
|
|
397
|
+
"""潮流引擎客户端(ctypes 直调 libipfpy)。"""
|
|
398
|
+
|
|
399
|
+
# ---- pf_get_list 枚举值(cflowlib.h enum pf_list_type)----
|
|
400
|
+
AREA_LIST, BUS_LIST, KV_LIST, OWNER_LIST, REC_TYPE_LIST, ZONE_LIST = \
|
|
401
|
+
range(6)
|
|
402
|
+
|
|
403
|
+
def __init__(self, dll=None, audit=None):
|
|
404
|
+
self._lib = _load_lib(dll or _find_dll())
|
|
405
|
+
self._setup_prototypes()
|
|
406
|
+
self._initialized = False
|
|
407
|
+
self._closed = False
|
|
408
|
+
# 引擎输出捕获(P0):最近一次捕获 + 累积环形缓冲
|
|
409
|
+
self._log_parts = []
|
|
410
|
+
self._log = bytearray()
|
|
411
|
+
# 快照字典(P1):tag -> 临时 .bse 路径
|
|
412
|
+
self._snapshots = {}
|
|
413
|
+
# 单步牛顿会话与实例级支路开断快照。
|
|
414
|
+
self._solution_session = "IDLE"
|
|
415
|
+
self._solution_config = {
|
|
416
|
+
"method": "newton", "decoupled": None,
|
|
417
|
+
"newton": None, "optim": None, "flat_start": None,
|
|
418
|
+
}
|
|
419
|
+
self._branch_snapshots = {}
|
|
420
|
+
# 查询缓存:load/change/solution 后统一失效,避免 solve_case
|
|
421
|
+
# 重复走逐记录 IPC。
|
|
422
|
+
self._cache = {}
|
|
423
|
+
# 审计日志(P0,可选): 文件路径或文件对象;记录 put_inrecs/
|
|
424
|
+
# solve_case 调用摘要(JSONL)
|
|
425
|
+
self._audit = audit
|
|
426
|
+
self._audit_owned = False
|
|
427
|
+
if audit is not None and not hasattr(audit, "write"):
|
|
428
|
+
self._audit = open(audit, "a", encoding="utf-8")
|
|
429
|
+
self._audit_owned = True
|
|
430
|
+
|
|
431
|
+
def _invalidate_cache(self):
|
|
432
|
+
"""清空查询缓存。**所有**改变算例、解或查询基准的入口都要调用。"""
|
|
433
|
+
self._cache.clear()
|
|
434
|
+
|
|
435
|
+
# ------------------------------------------------------------------
|
|
436
|
+
# 引擎输出捕获(P0)
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
def _append_log(self, data):
|
|
439
|
+
if not data:
|
|
440
|
+
return
|
|
441
|
+
self._log += data
|
|
442
|
+
if len(self._log) > _ENGINE_LOG_LIMIT:
|
|
443
|
+
del self._log[:len(self._log) - _ENGINE_LOG_LIMIT]
|
|
444
|
+
self._log_parts.append(data)
|
|
445
|
+
if len(self._log_parts) > 100:
|
|
446
|
+
del self._log_parts[:len(self._log_parts) - 100]
|
|
447
|
+
|
|
448
|
+
def capture(self):
|
|
449
|
+
"""上下文管理器: 引擎调用期间捕获 fd 1/2 输出。
|
|
450
|
+
|
|
451
|
+
用法::
|
|
452
|
+
with pf.capture():
|
|
453
|
+
rc = pf.solution()
|
|
454
|
+
log = pf.last_engine_log()
|
|
455
|
+
"""
|
|
456
|
+
return _EngineCapture(self._log_parts)
|
|
457
|
+
|
|
458
|
+
def last_engine_log(self, n=None):
|
|
459
|
+
"""最近一次 capture() 块内的引擎输出(str)。"""
|
|
460
|
+
parts = self._log_parts
|
|
461
|
+
if not parts:
|
|
462
|
+
return ""
|
|
463
|
+
text = b"".join(parts[-1:]).decode("ascii", "replace")
|
|
464
|
+
return text[-n:] if n else text
|
|
465
|
+
|
|
466
|
+
def engine_log(self, n=None):
|
|
467
|
+
"""自会话开始累积的引擎输出(str,环形缓冲尾部)。"""
|
|
468
|
+
if n:
|
|
469
|
+
return bytes(self._log[-n:]).decode("ascii", "replace")
|
|
470
|
+
return bytes(self._log).decode("ascii", "replace")
|
|
471
|
+
|
|
472
|
+
def _audit_log(self, event, detail=None):
|
|
473
|
+
if self._audit is None:
|
|
474
|
+
return
|
|
475
|
+
rec = {"t": time.time(), "event": event}
|
|
476
|
+
if detail:
|
|
477
|
+
rec.update(detail)
|
|
478
|
+
try:
|
|
479
|
+
self._audit.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
480
|
+
self._audit.flush()
|
|
481
|
+
except (OSError, ValueError):
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
# ------------------------------------------------------------------
|
|
485
|
+
# 变更卡批量提交(P0)
|
|
486
|
+
# ------------------------------------------------------------------
|
|
487
|
+
def put_inrecs(self, cards_list, capture=True):
|
|
488
|
+
"""批量提交变更卡(逐卡 pf_put_inrec)。
|
|
489
|
+
|
|
490
|
+
cards_list 为 [(描述, 卡文本), ...] 或 [卡文本, ...];
|
|
491
|
+
capture=True 时引擎调用输出被捕获(不刷屏)。
|
|
492
|
+
返回 {"n": 卡数, "rcs": [每卡 rc], "fail": [rc!=0 的描述]}
|
|
493
|
+
"""
|
|
494
|
+
if isinstance(cards_list, (str, bytes)):
|
|
495
|
+
raise TypeError(
|
|
496
|
+
"cards_list 必须是卡片序列;提交单张卡请使用 put_inrec(),"
|
|
497
|
+
"或传入 [card]")
|
|
498
|
+
seq = []
|
|
499
|
+
for item in cards_list:
|
|
500
|
+
if isinstance(item, (tuple, list)) and len(item) == 2:
|
|
501
|
+
if not isinstance(item[1], (str, bytes)):
|
|
502
|
+
raise TypeError("变更卡文本必须是 str 或 bytes")
|
|
503
|
+
seq.append((str(item[0]), item[1]))
|
|
504
|
+
else:
|
|
505
|
+
if not isinstance(item, (str, bytes)):
|
|
506
|
+
raise TypeError("变更卡必须是 str/bytes 或 (描述, 卡文本)")
|
|
507
|
+
seq.append(("", item))
|
|
508
|
+
rcs, fail = [], []
|
|
509
|
+
sink = []
|
|
510
|
+
cap = _EngineCapture(sink) if capture else None
|
|
511
|
+
|
|
512
|
+
def _run():
|
|
513
|
+
for desc, card in seq:
|
|
514
|
+
rc = self._lib.pf_put_inrec(_ascii(card))
|
|
515
|
+
rcs.append(rc)
|
|
516
|
+
if rc != 0:
|
|
517
|
+
preview = (_text(card) if isinstance(card, bytes)
|
|
518
|
+
else card)[:20]
|
|
519
|
+
fail.append(desc or preview)
|
|
520
|
+
|
|
521
|
+
if cap:
|
|
522
|
+
with cap:
|
|
523
|
+
_run()
|
|
524
|
+
else:
|
|
525
|
+
_run()
|
|
526
|
+
self._append_log(b"".join(sink))
|
|
527
|
+
self._invalidate_cache()
|
|
528
|
+
self._audit_log("put_inrecs", {"n": len(seq), "fail": fail})
|
|
529
|
+
return {"n": len(seq), "rcs": rcs, "fail": fail}
|
|
530
|
+
|
|
531
|
+
# ------------------------------------------------------------------
|
|
532
|
+
# LTC 变压器数据(直接读取内核 COMMON 块)
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
def ltc_data(self):
|
|
535
|
+
"""返回 LTC 变压器列表。
|
|
536
|
+
|
|
537
|
+
读取内核 /tran/ 公共块,不依赖 PFO 输出。每项包含变压器两端、
|
|
538
|
+
kctrl 原始控制码、受控母线映射、tap 位置/限值、Q 限值和变比侧。
|
|
539
|
+
tap 为内核标幺变比,tap_max/tap_min 同量纲;qmax_mvar/qmin_mvar
|
|
540
|
+
为 MVAr,仅对 RQ/RN 类控制有效。
|
|
541
|
+
"""
|
|
542
|
+
if "ltc" in self._cache:
|
|
543
|
+
return self._cache["ltc"]
|
|
544
|
+
ci = self.case_info()
|
|
545
|
+
nltc = c_int()
|
|
546
|
+
maxltc = max(int(ci.get("num_LTC_xfmrs", 0)) + 10, 64)
|
|
547
|
+
while True:
|
|
548
|
+
cbus1 = (c_char * (8 * maxltc))()
|
|
549
|
+
cbus2 = (c_char * (8 * maxltc))()
|
|
550
|
+
base1 = (c_float * maxltc)()
|
|
551
|
+
base2 = (c_float * maxltc)()
|
|
552
|
+
kctrl = (c_int * maxltc)()
|
|
553
|
+
tap_pos = (c_double * maxltc)()
|
|
554
|
+
tap_max = (c_float * maxltc)()
|
|
555
|
+
tap_min = (c_float * maxltc)()
|
|
556
|
+
tap_steps = (c_float * maxltc)()
|
|
557
|
+
rtype = (c_int * maxltc)()
|
|
558
|
+
lim_reached = (c_int * maxltc)()
|
|
559
|
+
qmax = (c_double * maxltc)()
|
|
560
|
+
qmin = (c_double * maxltc)()
|
|
561
|
+
num_taps = (c_int * maxltc)()
|
|
562
|
+
var_tap_side = (c_int * maxltc)()
|
|
563
|
+
use_ext = hasattr(self._lib, "ipfpy_get_ltc_ext")
|
|
564
|
+
if use_ext:
|
|
565
|
+
self._lib.ipfpy_get_ltc_ext(
|
|
566
|
+
byref(nltc), byref(c_int(maxltc)),
|
|
567
|
+
cbus1, cbus2, base1, base2, kctrl, tap_pos,
|
|
568
|
+
tap_max, tap_min, tap_steps, rtype, lim_reached,
|
|
569
|
+
qmax, qmin, num_taps, var_tap_side)
|
|
570
|
+
else:
|
|
571
|
+
self._lib.ipfpy_get_ltc(
|
|
572
|
+
byref(nltc), byref(c_int(maxltc)),
|
|
573
|
+
cbus1, cbus2, base1, base2, kctrl, tap_pos,
|
|
574
|
+
tap_max, tap_min, tap_steps, rtype, lim_reached)
|
|
575
|
+
if nltc.value < maxltc or maxltc >= MAXLTC:
|
|
576
|
+
break
|
|
577
|
+
maxltc = min(maxltc * 2, MAXLTC)
|
|
578
|
+
n = nltc.value
|
|
579
|
+
if n <= 0:
|
|
580
|
+
self._cache["ltc"] = []
|
|
581
|
+
return []
|
|
582
|
+
vd = self.voltages()
|
|
583
|
+
names, bases = vd["names"], vd["base"]
|
|
584
|
+
out = []
|
|
585
|
+
for i in range(n):
|
|
586
|
+
n1 = _text(bytes(cbus1[i * 8:(i + 1) * 8])).strip()
|
|
587
|
+
n2 = _text(bytes(cbus2[i * 8:(i + 1) * 8])).strip()
|
|
588
|
+
kc = kctrl[i]
|
|
589
|
+
ctrl_name = None
|
|
590
|
+
ctrl_kv = None
|
|
591
|
+
control_side = None
|
|
592
|
+
if 0 < kc <= len(names):
|
|
593
|
+
ctrl_name = names[kc - 1].strip()
|
|
594
|
+
ctrl_kv = bases[kc - 1]
|
|
595
|
+
control_side = "remote"
|
|
596
|
+
elif kc == -1:
|
|
597
|
+
ctrl_name, ctrl_kv, control_side = n1, base1[i], "bus1"
|
|
598
|
+
elif kc == -2:
|
|
599
|
+
ctrl_name, ctrl_kv, control_side = n2, base2[i], "bus2"
|
|
600
|
+
out.append({
|
|
601
|
+
"bus1": {"name": n1, "kv": base1[i]},
|
|
602
|
+
"bus2": {"name": n2, "kv": base2[i]},
|
|
603
|
+
"kctrl": kc,
|
|
604
|
+
"controlled_bus": ({"name": ctrl_name, "kv": ctrl_kv}
|
|
605
|
+
if ctrl_name is not None else None),
|
|
606
|
+
"control_side": control_side,
|
|
607
|
+
"tap": tap_pos[i],
|
|
608
|
+
"tap_max": tap_max[i],
|
|
609
|
+
"tap_min": tap_min[i],
|
|
610
|
+
"steps": tap_steps[i],
|
|
611
|
+
"num_taps": int(num_taps[i]) if use_ext else None,
|
|
612
|
+
"var_tap_side": (int(var_tap_side[i]) if use_ext else None),
|
|
613
|
+
"qmax_mvar": float(qmax[i]) if use_ext else None,
|
|
614
|
+
"qmin_mvar": float(qmin[i]) if use_ext else None,
|
|
615
|
+
"rtype": rtype[i],
|
|
616
|
+
"lim_reached": bool(lim_reached[i]),
|
|
617
|
+
})
|
|
618
|
+
self._cache["ltc"] = out
|
|
619
|
+
return out
|
|
620
|
+
|
|
621
|
+
# ------------------------------------------------------------------
|
|
622
|
+
# 结构数据与违规报告(P0)
|
|
623
|
+
# ------------------------------------------------------------------
|
|
624
|
+
def violations(self, v_tol=0.005, load_pct=1.0, q_tol=None,
|
|
625
|
+
capture=True):
|
|
626
|
+
"""扫描电压/支路/无功/LTC/孤岛违规。
|
|
627
|
+
|
|
628
|
+
过载门槛按支路类型取引擎负载率:L/E 用安培负载率,T/TP 用
|
|
629
|
+
MVA 负载率;R/RP 控制记录和 DC 线路不参与 AC 过载。load_pct
|
|
630
|
+
为比值,1.0 表示 100%。无功受限只检查
|
|
631
|
+
BQ/BG/BX 母线;B 类母线的 Qsch 是计划注入,BS 平衡机的 Qgen
|
|
632
|
+
是平衡结果,二者都不作为限值。
|
|
633
|
+
"""
|
|
634
|
+
return self._violations_from(
|
|
635
|
+
self._buses_all(), self._branches_all(False),
|
|
636
|
+
v_tol=v_tol, load_pct=load_pct, q_tol=q_tol)
|
|
637
|
+
|
|
638
|
+
def _violations_from(self, buses, branches, v_tol=0.005,
|
|
639
|
+
load_pct=1.0, q_tol=None):
|
|
640
|
+
v_under, v_over, overload, q_limited = [], [], [], []
|
|
641
|
+
for b in buses:
|
|
642
|
+
btype = (b.get("type") or "").strip().upper()
|
|
643
|
+
if b.get("is_dc") or btype in ("BD", "BM"):
|
|
644
|
+
continue
|
|
645
|
+
kv = float(b.get("kv") or 0.0)
|
|
646
|
+
if kv <= 0.0:
|
|
647
|
+
continue
|
|
648
|
+
vmag_pu = float(b.get("Vmag") or 0.0) / kv
|
|
649
|
+
vmax = float(b.get("Vhold_Vmax") or 0.0)
|
|
650
|
+
vmin = float(b.get("Vmin_Vdeg") or 0.0)
|
|
651
|
+
if btype == "BS":
|
|
652
|
+
# BS 母线的 Vmin_Vdeg 字段保存电压角,不是电压下限。
|
|
653
|
+
vmin = 0.0
|
|
654
|
+
if vmin > 0.0 and vmag_pu < vmin - v_tol:
|
|
655
|
+
v_under.append({"bus": b["name"], "kv": b["kv"],
|
|
656
|
+
"vmag": vmag_pu, "limit": vmin,
|
|
657
|
+
"bus_type": btype})
|
|
658
|
+
if vmax > 0.0 and vmag_pu > vmax + v_tol:
|
|
659
|
+
v_over.append({"bus": b["name"], "kv": b["kv"],
|
|
660
|
+
"vmag": vmag_pu, "limit": vmax,
|
|
661
|
+
"bus_type": btype})
|
|
662
|
+
if btype not in ("BQ", "BG", "BX"):
|
|
663
|
+
continue
|
|
664
|
+
qmax = float(b.get("Qsch_Qmax") or 0.0)
|
|
665
|
+
qmin = float(b.get("Qmin") or 0.0)
|
|
666
|
+
if qmax <= 0.0 and qmin >= 0.0:
|
|
667
|
+
continue
|
|
668
|
+
tol = q_tol if q_tol is not None else max(
|
|
669
|
+
0.5, 0.01 * max(abs(qmax), abs(qmin)))
|
|
670
|
+
qgen = float(b.get("Qgen") or 0.0)
|
|
671
|
+
if qgen >= qmax - tol or qgen <= qmin + tol:
|
|
672
|
+
q_limited.append({"bus": b["name"], "kv": b["kv"],
|
|
673
|
+
"qgen": qgen, "qmax": qmax,
|
|
674
|
+
"qmin": qmin, "bus_type": btype})
|
|
675
|
+
|
|
676
|
+
seen = set()
|
|
677
|
+
for br in branches:
|
|
678
|
+
if not br.get("solution_available", True):
|
|
679
|
+
continue
|
|
680
|
+
key = _branch_dedup_key(br)
|
|
681
|
+
if key in seen:
|
|
682
|
+
continue
|
|
683
|
+
seen.add(key)
|
|
684
|
+
ratio = br.get("loading_ratio")
|
|
685
|
+
if ratio is None:
|
|
686
|
+
continue
|
|
687
|
+
if ratio > load_pct:
|
|
688
|
+
overload.append({
|
|
689
|
+
"bus1": br["bus1_name"].strip(),
|
|
690
|
+
"bus2": br["bus2_name"].strip(),
|
|
691
|
+
"ckt": br["ckt"],
|
|
692
|
+
"pct": ratio,
|
|
693
|
+
"loading_pct": ratio * 100.0,
|
|
694
|
+
"s_mva": br.get("s_mva"),
|
|
695
|
+
"i_amps": br.get("i_amps"),
|
|
696
|
+
"rating": br.get("rating_used"),
|
|
697
|
+
"rating_code": br.get("rating_code"),
|
|
698
|
+
"loading_kind": br.get("loading_kind"),
|
|
699
|
+
"section": br.get("section", 0),
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
ltc_limited = []
|
|
703
|
+
for l in self.ltc_data():
|
|
704
|
+
if not l["lim_reached"]:
|
|
705
|
+
continue
|
|
706
|
+
ltc_limited.append({
|
|
707
|
+
"bus1": l["bus1"]["name"],
|
|
708
|
+
"bus2": l["bus2"]["name"],
|
|
709
|
+
"controlled_bus": l.get("controlled_bus"),
|
|
710
|
+
"control_side": l.get("control_side"),
|
|
711
|
+
"tap": l["tap"],
|
|
712
|
+
"tap_min": l["tap_min"],
|
|
713
|
+
"tap_max": l["tap_max"],
|
|
714
|
+
"steps": l["steps"],
|
|
715
|
+
"qmax_mvar": l.get("qmax_mvar"),
|
|
716
|
+
"qmin_mvar": l.get("qmin_mvar"),
|
|
717
|
+
"rtype": l["rtype"],
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
islands = self._find_islands(buses, branches)
|
|
721
|
+
return {
|
|
722
|
+
"v_under": v_under, "v_over": v_over,
|
|
723
|
+
"overload": overload, "q_limited": q_limited,
|
|
724
|
+
"ltc_limited": ltc_limited, "islands": islands,
|
|
725
|
+
"counts": {"v_under": len(v_under), "v_over": len(v_over),
|
|
726
|
+
"overload": len(overload),
|
|
727
|
+
"q_limited": len(q_limited),
|
|
728
|
+
"ltc_limited": len(ltc_limited),
|
|
729
|
+
"islands": len(islands)},
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
def _find_islands(self, buses=None, branches=None):
|
|
733
|
+
"""检测不含平衡母线(BS)的连通分量(孤岛)。
|
|
734
|
+
|
|
735
|
+
节点 = 当前活动的 AC 母线,边 = 当前活动支路。停运支路不参与
|
|
736
|
+
连通性。返回 [{"buses": [...], "pgen_mw", "pload_mw",
|
|
737
|
+
"n_buses"}, ...]。
|
|
738
|
+
"""
|
|
739
|
+
if buses is None:
|
|
740
|
+
buses = self._buses_all()
|
|
741
|
+
if branches is None:
|
|
742
|
+
branches = self._branches_all(False)
|
|
743
|
+
if not buses:
|
|
744
|
+
return []
|
|
745
|
+
key = lambda b: (b["name"].strip().upper(), round(b["kv"], 3))
|
|
746
|
+
nodes = [key(b) for b in buses
|
|
747
|
+
if not b.get("is_dc")
|
|
748
|
+
and (b.get("type") or "").strip().upper() not in ("BD", "BM")]
|
|
749
|
+
adj = {n: [] for n in nodes}
|
|
750
|
+
for br in branches:
|
|
751
|
+
if not br.get("solution_available", True):
|
|
752
|
+
continue
|
|
753
|
+
if br.get("branch_kind") not in ("line", "transformer"):
|
|
754
|
+
continue
|
|
755
|
+
a = (br["bus1_name"].strip().upper(), round(br["kv1"], 3))
|
|
756
|
+
b = (br["bus2_name"].strip().upper(), round(br["kv2"], 3))
|
|
757
|
+
if a in adj and b in adj:
|
|
758
|
+
adj[a].append(b)
|
|
759
|
+
adj[b].append(a)
|
|
760
|
+
seen = set()
|
|
761
|
+
comps = []
|
|
762
|
+
for n in nodes:
|
|
763
|
+
if n in seen:
|
|
764
|
+
continue
|
|
765
|
+
comp, stack = [], [n]
|
|
766
|
+
seen.add(n)
|
|
767
|
+
while stack:
|
|
768
|
+
cur = stack.pop()
|
|
769
|
+
comp.append(cur)
|
|
770
|
+
for nb in adj.get(cur, ()):
|
|
771
|
+
if nb not in seen:
|
|
772
|
+
seen.add(nb)
|
|
773
|
+
stack.append(nb)
|
|
774
|
+
comps.append(comp)
|
|
775
|
+
slack_keys = set(
|
|
776
|
+
key(b) for b in buses
|
|
777
|
+
if b["type"].strip().upper() == "BS")
|
|
778
|
+
busmap = {key(b): b for b in buses}
|
|
779
|
+
islands = []
|
|
780
|
+
for comp in comps:
|
|
781
|
+
if any(c in slack_keys for c in comp):
|
|
782
|
+
continue
|
|
783
|
+
pgen = sum(busmap[c]["Pgen"] for c in comp if c in busmap)
|
|
784
|
+
pload = sum(busmap[c]["Pload"] for c in comp if c in busmap)
|
|
785
|
+
islands.append({
|
|
786
|
+
"buses": [{"name": c[0], "kv": c[1]}
|
|
787
|
+
for c in sorted(comp)],
|
|
788
|
+
"n_buses": len(comp),
|
|
789
|
+
"pgen_mw": pgen, "pload_mw": pload,
|
|
790
|
+
})
|
|
791
|
+
return islands
|
|
792
|
+
|
|
793
|
+
def solve_case(self, changes=None, capture=True, v_tol=0.005,
|
|
794
|
+
load_pct=1.0, auto_rollback=True,
|
|
795
|
+
include_violations=True, include_loss=True,
|
|
796
|
+
include_voltage_summary=True):
|
|
797
|
+
"""施加变更卡(可选)并求解,返回结构化结果。
|
|
798
|
+
|
|
799
|
+
changes: Changeset 实例或 [(描述, 卡)] 列表;
|
|
800
|
+
auto_rollback: True 时变更提交前自动快照,失败时自动回滚;
|
|
801
|
+
include_violations/include_loss/include_voltage_summary:
|
|
802
|
+
批量场景可关闭不需要的后处理,显著减少逐记录查询。
|
|
803
|
+
返回 dict:
|
|
804
|
+
rc / converged 求解返回码 / 是否收敛
|
|
805
|
+
mismatch 最终偏差(MW/MVAr)
|
|
806
|
+
iterations 迭代计数
|
|
807
|
+
vmin/vmax 全网电压上下限(pu,含所在母线)
|
|
808
|
+
worst_loading 最重载支路(比值为 1.0 表示 100%)
|
|
809
|
+
loss 网损(MW/MVAr)
|
|
810
|
+
violations violations() 结果;跳过时为 None
|
|
811
|
+
engine_log capture=True 时的引擎输出
|
|
812
|
+
rolled_back auto_rollback 触发回滚时为 True
|
|
813
|
+
"""
|
|
814
|
+
rc = 0
|
|
815
|
+
sink = []
|
|
816
|
+
cap = _EngineCapture(sink) if capture else None
|
|
817
|
+
snap_tag = None
|
|
818
|
+
|
|
819
|
+
def _run():
|
|
820
|
+
nonlocal rc
|
|
821
|
+
if changes is not None:
|
|
822
|
+
if isinstance(changes, _Changeset):
|
|
823
|
+
seq = list(changes.cards())
|
|
824
|
+
else:
|
|
825
|
+
if isinstance(changes, (str, bytes)):
|
|
826
|
+
raise PyFlowError(
|
|
827
|
+
"changes 必须是 Changeset 或 (描述, 卡文本) 序列")
|
|
828
|
+
seq = list(changes)
|
|
829
|
+
for item in seq:
|
|
830
|
+
if not isinstance(item, (tuple, list)) or len(item) != 2:
|
|
831
|
+
raise PyFlowError(
|
|
832
|
+
"changes 的每一项必须是 (描述, 卡文本)")
|
|
833
|
+
desc, card = item
|
|
834
|
+
if not isinstance(card, (str, bytes)):
|
|
835
|
+
raise PyFlowError("变更卡文本必须是 str 或 bytes")
|
|
836
|
+
r = _submit_card(self, card)
|
|
837
|
+
if r != 0:
|
|
838
|
+
raise PyFlowError(
|
|
839
|
+
"变更卡提交失败 rc=%d: %s\n%s"
|
|
840
|
+
% (r, desc, card))
|
|
841
|
+
rc = self._lib.pf_solution()
|
|
842
|
+
|
|
843
|
+
if changes is not None and auto_rollback:
|
|
844
|
+
try:
|
|
845
|
+
snap_tag = self.snapshot()
|
|
846
|
+
except PyFlowError:
|
|
847
|
+
snap_tag = None
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
if cap:
|
|
851
|
+
with cap:
|
|
852
|
+
_run()
|
|
853
|
+
else:
|
|
854
|
+
_run()
|
|
855
|
+
except PyFlowError as e:
|
|
856
|
+
if snap_tag is not None:
|
|
857
|
+
try:
|
|
858
|
+
self.restore(snap_tag)
|
|
859
|
+
finally:
|
|
860
|
+
self.drop_snapshot(snap_tag)
|
|
861
|
+
rpt = {
|
|
862
|
+
"rc": -1,
|
|
863
|
+
"converged": False,
|
|
864
|
+
"error": str(e),
|
|
865
|
+
"rolled_back": snap_tag is not None,
|
|
866
|
+
"engine_log": b"".join(sink).decode("ascii", "replace")
|
|
867
|
+
if capture else "",
|
|
868
|
+
}
|
|
869
|
+
return rpt
|
|
870
|
+
|
|
871
|
+
self._invalidate_cache()
|
|
872
|
+
self._append_log(b"".join(sink))
|
|
873
|
+
|
|
874
|
+
m = self.mismatch()
|
|
875
|
+
rpt = {
|
|
876
|
+
"rc": rc,
|
|
877
|
+
"converged": rc == 0,
|
|
878
|
+
"mismatch": {"dp_mw": m["dptot"], "dq_mvar": m["dqtot"],
|
|
879
|
+
"total": m["dttot"], "angle": m["datot"]},
|
|
880
|
+
"iterations": {"total": m["ittot"],
|
|
881
|
+
"decoupled": m["itrhis_decoupled"],
|
|
882
|
+
"nr": m["itrhis_nr"]},
|
|
883
|
+
"violations": None,
|
|
884
|
+
"violations_skipped": not include_violations,
|
|
885
|
+
"loss": None,
|
|
886
|
+
"worst_loading": None,
|
|
887
|
+
}
|
|
888
|
+
if rc == 0:
|
|
889
|
+
buses = (self._buses_all()
|
|
890
|
+
if (include_voltage_summary or include_violations)
|
|
891
|
+
else None)
|
|
892
|
+
branches = (self._branches_all(False)
|
|
893
|
+
if (include_loss or include_violations)
|
|
894
|
+
else None)
|
|
895
|
+
if include_voltage_summary:
|
|
896
|
+
vmin_b = vmax_b = None
|
|
897
|
+
vmin = float("inf")
|
|
898
|
+
vmax = float("-inf")
|
|
899
|
+
for b in buses:
|
|
900
|
+
if b.get("is_dc"):
|
|
901
|
+
continue
|
|
902
|
+
kv = float(b.get("kv") or 0.0)
|
|
903
|
+
if kv <= 0.0:
|
|
904
|
+
continue
|
|
905
|
+
vmag_pu = float(b.get("Vmag") or 0.0) / kv
|
|
906
|
+
if vmag_pu < vmin:
|
|
907
|
+
vmin, vmin_b = vmag_pu, b["name"]
|
|
908
|
+
if vmag_pu > vmax:
|
|
909
|
+
vmax, vmax_b = vmag_pu, b["name"]
|
|
910
|
+
rpt["vmin"], rpt["vmax"] = vmin, vmax
|
|
911
|
+
rpt["vmin_bus"], rpt["vmax_bus"] = vmin_b, vmax_b
|
|
912
|
+
if include_loss:
|
|
913
|
+
worst, who, kind, code = 0.0, "", None, None
|
|
914
|
+
p_loss, q_loss = 0.0, 0.0
|
|
915
|
+
seen = set()
|
|
916
|
+
for br in branches:
|
|
917
|
+
if not br.get("solution_available", True):
|
|
918
|
+
continue
|
|
919
|
+
if br.get("branch_kind") not in ("line", "transformer"):
|
|
920
|
+
continue
|
|
921
|
+
key = _branch_dedup_key(br)
|
|
922
|
+
if key in seen:
|
|
923
|
+
continue
|
|
924
|
+
seen.add(key)
|
|
925
|
+
p_loss += br["Ploss"]
|
|
926
|
+
q_loss += br["Qloss"]
|
|
927
|
+
ratio = br.get("loading_ratio")
|
|
928
|
+
if ratio is not None and ratio > worst:
|
|
929
|
+
worst = ratio
|
|
930
|
+
who = "%s-%s %s" % (
|
|
931
|
+
br["bus1_name"].strip(),
|
|
932
|
+
br["bus2_name"].strip(), br["ckt"])
|
|
933
|
+
kind = br.get("loading_kind")
|
|
934
|
+
code = br.get("rating_code")
|
|
935
|
+
rpt["worst_loading"] = {
|
|
936
|
+
"pct": worst, "loading_pct": worst * 100.0,
|
|
937
|
+
"branch": who, "loading_kind": kind,
|
|
938
|
+
"rating_code": code,
|
|
939
|
+
}
|
|
940
|
+
rpt["loss"] = {"p_mw": p_loss, "q_mvar": q_loss}
|
|
941
|
+
if include_violations:
|
|
942
|
+
rpt["violations"] = self._violations_from(
|
|
943
|
+
buses, branches, v_tol=v_tol, load_pct=load_pct)
|
|
944
|
+
else:
|
|
945
|
+
if include_violations:
|
|
946
|
+
rpt["violations"] = {
|
|
947
|
+
"counts": {"v_under": 0, "v_over": 0,
|
|
948
|
+
"overload": 0, "q_limited": 0,
|
|
949
|
+
"ltc_limited": 0, "islands": 0}}
|
|
950
|
+
if capture:
|
|
951
|
+
rpt["engine_log"] = b"".join(sink).decode("ascii", "replace")
|
|
952
|
+
if snap_tag is not None:
|
|
953
|
+
rolled_back = False
|
|
954
|
+
try:
|
|
955
|
+
if rc != 0:
|
|
956
|
+
self.restore(snap_tag)
|
|
957
|
+
rolled_back = True
|
|
958
|
+
finally:
|
|
959
|
+
self.drop_snapshot(snap_tag)
|
|
960
|
+
rpt["rolled_back"] = rolled_back
|
|
961
|
+
self._audit_log("solve_case", {"rc": rc, "converged": rc == 0})
|
|
962
|
+
return rpt
|
|
963
|
+
|
|
964
|
+
# ------------------------------------------------------------------
|
|
965
|
+
# PFO 报告(P1,封装引擎标准输出报告)
|
|
966
|
+
# ------------------------------------------------------------------
|
|
967
|
+
def write_pfo(self, path):
|
|
968
|
+
"""将引擎当前解的标准输出报告写入文件。
|
|
969
|
+
|
|
970
|
+
求解一次(如已求解则快速返回)并将 stdout 捕获写至 path。
|
|
971
|
+
返回 rc。
|
|
972
|
+
"""
|
|
973
|
+
with self.capture():
|
|
974
|
+
rc = self._lib.pf_solution()
|
|
975
|
+
self._invalidate_cache()
|
|
976
|
+
log = self.last_engine_log()
|
|
977
|
+
with open(path, "w", encoding="ascii", errors="replace") as f:
|
|
978
|
+
f.write(log)
|
|
979
|
+
return rc
|
|
980
|
+
|
|
981
|
+
def parse_pfo(self, path):
|
|
982
|
+
"""解析 PFO 文件为结构化 dict(不触发求解)。
|
|
983
|
+
|
|
984
|
+
PFO 文件的格式为引擎标准输出报告,含求解参数、迭代表、
|
|
985
|
+
母线/支路统计、收敛状态等。此解析器提取关键字段。
|
|
986
|
+
"""
|
|
987
|
+
with open(path, encoding="ascii", errors="replace") as f:
|
|
988
|
+
text = f.read()
|
|
989
|
+
return self._parse_pfo_text(text)
|
|
990
|
+
|
|
991
|
+
def report_pfo(self, path=None):
|
|
992
|
+
"""生成 PFO 报告并解析为结构化 dict。
|
|
993
|
+
|
|
994
|
+
用法::
|
|
995
|
+
rpt = pf.report_pfo() # 用当前缓存的引擎输出
|
|
996
|
+
rpt = pf.report_pfo("case.pfo") # 写入文件 + 解析
|
|
997
|
+
|
|
998
|
+
返回 dict:
|
|
999
|
+
converged 是否收敛
|
|
1000
|
+
rc 求解返回码
|
|
1001
|
+
n_buses/n_branches 母线/支路统计
|
|
1002
|
+
iterations 迭代计数(含各类型)
|
|
1003
|
+
final_mismatch 最终 P/Q 偏差
|
|
1004
|
+
warnings 警告消息列表
|
|
1005
|
+
raw_text 完整原始文本
|
|
1006
|
+
"""
|
|
1007
|
+
if path:
|
|
1008
|
+
rc = self.write_pfo(path)
|
|
1009
|
+
text = open(path, encoding="ascii", errors="replace").read()
|
|
1010
|
+
else:
|
|
1011
|
+
log = self.last_engine_log()
|
|
1012
|
+
if not log:
|
|
1013
|
+
with self.capture():
|
|
1014
|
+
rc = self._lib.pf_solution()
|
|
1015
|
+
self._invalidate_cache()
|
|
1016
|
+
log = self.last_engine_log()
|
|
1017
|
+
else:
|
|
1018
|
+
rc = 0
|
|
1019
|
+
text = log
|
|
1020
|
+
return self._parse_pfo_text(text, rc=rc)
|
|
1021
|
+
|
|
1022
|
+
@staticmethod
|
|
1023
|
+
def _parse_ltc_line(line):
|
|
1024
|
+
"""解析 PFO "Summary of LTC Transformers" 段中的一行(ltcsum.f 160 格式)。
|
|
1025
|
+
|
|
1026
|
+
定宽列(0 索引):
|
|
1027
|
+
1-8 被控母线名 a8
|
|
1028
|
+
9-14 被控母线基准 f6.1
|
|
1029
|
+
17-34 vmin/vmax/vact 3f6.3
|
|
1030
|
+
37-44 bus1(T 卡首端)a8
|
|
1031
|
+
45-50 bus1 基准 f6.1
|
|
1032
|
+
52-59 bus2(T 卡末端)a8
|
|
1033
|
+
60-65 bus2 基准 f6.1
|
|
1034
|
+
68-80 tap1/tap2 'xxx.xx/xxx.xx'
|
|
1035
|
+
81-88 tapmin f8.2
|
|
1036
|
+
89-96 tapmax f8.2
|
|
1037
|
+
97-102 istep '/' itstep
|
|
1038
|
+
104+ 状态标记('TMIN limit reached' / 'TMAX limit reached' /
|
|
1039
|
+
'V violation =x.xxx')
|
|
1040
|
+
返回 dict 或 None(行格式不符)。
|
|
1041
|
+
"""
|
|
1042
|
+
def _f(s):
|
|
1043
|
+
s = s.strip()
|
|
1044
|
+
return float(s) if s else None
|
|
1045
|
+
|
|
1046
|
+
import re
|
|
1047
|
+
try:
|
|
1048
|
+
item = {
|
|
1049
|
+
"controlled_bus": line[1:9].strip(),
|
|
1050
|
+
"controlled_kv": _f(line[9:15]),
|
|
1051
|
+
"bus1": line[37:45].strip(),
|
|
1052
|
+
"kv1": _f(line[45:51]),
|
|
1053
|
+
"bus2": line[52:60].strip(),
|
|
1054
|
+
"kv2": _f(line[60:66]),
|
|
1055
|
+
"vmin": _f(line[17:23]),
|
|
1056
|
+
"vmax": _f(line[23:29]),
|
|
1057
|
+
"v_actual": _f(line[29:35]),
|
|
1058
|
+
"tap1": None, "tap2": None,
|
|
1059
|
+
"tapmin": _f(line[81:89]),
|
|
1060
|
+
"tapmax": _f(line[89:97]),
|
|
1061
|
+
}
|
|
1062
|
+
except ValueError:
|
|
1063
|
+
return None
|
|
1064
|
+
tap_pair = line[68:81].strip()
|
|
1065
|
+
if "/" in tap_pair:
|
|
1066
|
+
t1, t2 = tap_pair.split("/", 1)
|
|
1067
|
+
item["tap1"] = _f(t1)
|
|
1068
|
+
item["tap2"] = _f(t2)
|
|
1069
|
+
if "TMIN limit reached" in line:
|
|
1070
|
+
item["limit"] = "TMIN"
|
|
1071
|
+
elif "TMAX limit reached" in line:
|
|
1072
|
+
item["limit"] = "TMAX"
|
|
1073
|
+
elif "V violation" in line:
|
|
1074
|
+
item["limit"] = "VVIOL"
|
|
1075
|
+
try:
|
|
1076
|
+
item["violation_mw_kv"] = _f(
|
|
1077
|
+
line[104:].split("=", 1)[1].strip())
|
|
1078
|
+
except (IndexError, ValueError):
|
|
1079
|
+
pass
|
|
1080
|
+
else:
|
|
1081
|
+
item["limit"] = None
|
|
1082
|
+
m = re.match(r"(\d+)/(\d+)", line[97:103].strip())
|
|
1083
|
+
if m:
|
|
1084
|
+
item["step"] = int(m.group(1))
|
|
1085
|
+
item["nsteps"] = int(m.group(2))
|
|
1086
|
+
return item
|
|
1087
|
+
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _parse_pfo_text(text, rc=0):
|
|
1090
|
+
"""解析 PFO 文本,提取关键字段。
|
|
1091
|
+
|
|
1092
|
+
返回 dict:
|
|
1093
|
+
converged 是否收敛
|
|
1094
|
+
rc 求解返回码
|
|
1095
|
+
n_buses/n_branches 母线/支路统计
|
|
1096
|
+
iterations 迭代计数
|
|
1097
|
+
final_mismatch 最终 P/Q 偏差
|
|
1098
|
+
vmin/vmax 解后电压上下限(pu)
|
|
1099
|
+
worst_loading 最重载支路
|
|
1100
|
+
loss 网损 MW+MVAr
|
|
1101
|
+
area_interchange 区域交换摘要
|
|
1102
|
+
ltc_summary LTC 统计摘要
|
|
1103
|
+
warnings 警告消息列表
|
|
1104
|
+
raw_text 完整原始文本
|
|
1105
|
+
"""
|
|
1106
|
+
import re
|
|
1107
|
+
result = {"rc": rc, "converged": False, "warnings": []}
|
|
1108
|
+
lines = text.split("\n")
|
|
1109
|
+
|
|
1110
|
+
# 母线/支路计数
|
|
1111
|
+
for m in re.finditer(r"(\d+)\s+BUSES", text):
|
|
1112
|
+
result["n_buses"] = int(m.group(1))
|
|
1113
|
+
for m in re.finditer(r"(\d+)\s+BRANCHES", text):
|
|
1114
|
+
result["n_branches"] = int(m.group(1))
|
|
1115
|
+
for m in re.finditer(r"(\d+)\s+--\s+LTC\s+TRANSFORMERS", text):
|
|
1116
|
+
result["ltc_summary"] = {"n_ltc": int(m.group(1))}
|
|
1117
|
+
for m in re.finditer(r"(\d+)\s+--\s+SLACK\s+BUSES", text):
|
|
1118
|
+
result.setdefault("ltc_summary", {})["n_slack"] = int(m.group(1))
|
|
1119
|
+
|
|
1120
|
+
# "Summary of LTC Transformers" 段(ltcsum.f 160 格式):
|
|
1121
|
+
# '0' + 被控母线 a8 + 基准 f6.1 + 3f6.3(电压限/实际) + bus_k + base_k
|
|
1122
|
+
# + bus_m + base_m + tap1/tap2(列 69 起) + tapmin/tapmax(列 82/90 起)
|
|
1123
|
+
# + istep/itstep + 行尾状态标记(TMIN/TMAX limit reached、
|
|
1124
|
+
# V violation =x.xxx)
|
|
1125
|
+
for i, line in enumerate(lines):
|
|
1126
|
+
if "Summary of LTC Transformers" in line:
|
|
1127
|
+
ltc = result.setdefault("ltc_summary", {})
|
|
1128
|
+
ltc.setdefault("limited", [])
|
|
1129
|
+
j = i + 1
|
|
1130
|
+
while j < len(lines) and (
|
|
1131
|
+
"min max actual" in lines[j]
|
|
1132
|
+
or "Controlling" in lines[j]
|
|
1133
|
+
or "Tap" in lines[j]
|
|
1134
|
+
or "----" in lines[j]
|
|
1135
|
+
or not lines[j].strip()):
|
|
1136
|
+
j += 1
|
|
1137
|
+
while j < len(lines):
|
|
1138
|
+
l = lines[j]
|
|
1139
|
+
if not l.startswith("0") or "/" not in l:
|
|
1140
|
+
break
|
|
1141
|
+
if "limit reached" not in l and "V violation" not in l:
|
|
1142
|
+
j += 1
|
|
1143
|
+
continue
|
|
1144
|
+
item = BPAFlow._parse_ltc_line(l)
|
|
1145
|
+
if item:
|
|
1146
|
+
if "limit reached" in l:
|
|
1147
|
+
ltc["limited"].append(item)
|
|
1148
|
+
j += 1
|
|
1149
|
+
break
|
|
1150
|
+
|
|
1151
|
+
# 收敛状态
|
|
1152
|
+
if "SUCCESSFUL SOLUTION REACHED" in text:
|
|
1153
|
+
result["converged"] = True
|
|
1154
|
+
elif "SOLUTION DID NOT CONVERGE" in text:
|
|
1155
|
+
result["converged"] = False
|
|
1156
|
+
elif rc != 0:
|
|
1157
|
+
result["converged"] = False
|
|
1158
|
+
|
|
1159
|
+
# 迭代表行 → 最终偏差
|
|
1160
|
+
iter_rows = []
|
|
1161
|
+
for line in lines:
|
|
1162
|
+
parts = line.strip().split()
|
|
1163
|
+
if len(parts) >= 6 and parts[0].isdigit():
|
|
1164
|
+
try:
|
|
1165
|
+
p_err = float(parts[1])
|
|
1166
|
+
q_err = float(parts[2])
|
|
1167
|
+
iter_rows.append((int(parts[0]), p_err, q_err))
|
|
1168
|
+
except (ValueError, IndexError):
|
|
1169
|
+
pass
|
|
1170
|
+
if iter_rows:
|
|
1171
|
+
last = iter_rows[-1]
|
|
1172
|
+
result["final_mismatch"] = {
|
|
1173
|
+
"P_mw": last[1], "Q_mvar": last[2],
|
|
1174
|
+
"iterations": last[0],
|
|
1175
|
+
}
|
|
1176
|
+
result["iterations"] = {"total": len(iter_rows)}
|
|
1177
|
+
|
|
1178
|
+
# 迭代数与总线/支路统计中提取
|
|
1179
|
+
for m in re.finditer(
|
|
1180
|
+
r"ITERATION\s+.*\(+\s*(\d+)\s*\)\s+Decoupled\s+"
|
|
1181
|
+
r".*\(+\s*(\d+)\s*\)\s+Modified\s+Newton\s+"
|
|
1182
|
+
r".*\(+\s*(\d+)\s*\)\s+Newton",
|
|
1183
|
+
text):
|
|
1184
|
+
result.setdefault("iterations", {})
|
|
1185
|
+
result["iterations"].update({
|
|
1186
|
+
"max": int(m.group(1)),
|
|
1187
|
+
"decoupled": int(m.group(2)),
|
|
1188
|
+
"modified_newton": int(m.group(3)),
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
# 警告消息
|
|
1192
|
+
for line in lines:
|
|
1193
|
+
s = line.strip("* \r").strip()
|
|
1194
|
+
if s.startswith("WARNING") or " WARNING " in line:
|
|
1195
|
+
result["warnings"].append(s)
|
|
1196
|
+
|
|
1197
|
+
# 网损(从后往前找 "LOSSES" 或 "Ploss" 不适合,PFO 不含明细)
|
|
1198
|
+
# 高/低电压与重载支路由 solve_case 提供,PFO 不直接含
|
|
1199
|
+
|
|
1200
|
+
return result
|
|
1201
|
+
|
|
1202
|
+
def changes(self):
|
|
1203
|
+
"""新建一个 Changeset(变更卡集合 + 快照 + 校验)。"""
|
|
1204
|
+
return _Changeset(self)
|
|
1205
|
+
|
|
1206
|
+
# ---- 高层动作便捷方法(P0;委托给 actions.Changeset)------------
|
|
1207
|
+
def _one_shot(self, method, *args, **kwargs):
|
|
1208
|
+
self._ensure_mutation_allowed()
|
|
1209
|
+
cs = _Changeset(self)
|
|
1210
|
+
getattr(cs, method)(*args, **kwargs)
|
|
1211
|
+
return cs.apply()
|
|
1212
|
+
|
|
1213
|
+
def set_gen_p(self, name, kv, pgen):
|
|
1214
|
+
"""设置母线有功出力调度(MW)并提交。"""
|
|
1215
|
+
return self._one_shot("set_gen_p", name, kv, pgen)
|
|
1216
|
+
|
|
1217
|
+
def set_gen_q_limits(self, name, kv, qmax=None, qmin=None):
|
|
1218
|
+
"""设置无功出力上下限(MVAr)并提交。"""
|
|
1219
|
+
return self._one_shot("set_gen_q_limits", name, kv, qmax=qmax,
|
|
1220
|
+
qmin=qmin)
|
|
1221
|
+
|
|
1222
|
+
def set_q_sched(self, name, kv, q):
|
|
1223
|
+
"""设置母线无功出力调度(MVAr)并提交。"""
|
|
1224
|
+
return self._one_shot("set_q_sched", name, kv, q)
|
|
1225
|
+
|
|
1226
|
+
def set_volt_target(self, name, kv, vmax=None, vmin=None):
|
|
1227
|
+
"""设置母线电压上下限(pu)并提交。"""
|
|
1228
|
+
return self._one_shot("set_volt_target", name, kv, vmax=vmax,
|
|
1229
|
+
vmin=vmin)
|
|
1230
|
+
|
|
1231
|
+
def set_load(self, name, kv, pload=None, qload=None):
|
|
1232
|
+
"""设置母线负荷(MW/MVAr)并提交。"""
|
|
1233
|
+
return self._one_shot("set_load", name, kv, pload=pload,
|
|
1234
|
+
qload=qload)
|
|
1235
|
+
|
|
1236
|
+
def set_bus_generation(self, name, kv, p_mw, allow_slack=False):
|
|
1237
|
+
"""设置 BPA B 卡母线的聚合有功出力。
|
|
1238
|
+
|
|
1239
|
+
该数据模型没有机组 ID,因此本方法明确采用母线聚合语义;默认
|
|
1240
|
+
拒绝修改平衡母线,避免求解器重新分配后造成“设置未生效”错觉。
|
|
1241
|
+
"""
|
|
1242
|
+
bus = self.bus(name, kv)
|
|
1243
|
+
if bus is None:
|
|
1244
|
+
raise PyFlowError("母线 %s %g kV 不存在" % (name, kv))
|
|
1245
|
+
if bus["type"].strip().upper() == "BS" and not allow_slack:
|
|
1246
|
+
raise PyFlowError(
|
|
1247
|
+
"平衡母线出力由潮流平衡决定;确需改卡请显式传 "
|
|
1248
|
+
"allow_slack=True")
|
|
1249
|
+
result = self.set_gen_p(name, kv, float(p_mw))
|
|
1250
|
+
result["semantics"] = "aggregate_bus_generation"
|
|
1251
|
+
return result
|
|
1252
|
+
|
|
1253
|
+
def set_bus_load(self, name, kv, p_mw=None, q_mvar=None):
|
|
1254
|
+
"""设置 BPA B 卡母线的聚合负荷(MW/MVAr)。"""
|
|
1255
|
+
if p_mw is None and q_mvar is None:
|
|
1256
|
+
raise ValueError("p_mw 与 q_mvar 至少指定一个")
|
|
1257
|
+
if self.bus(name, kv) is None:
|
|
1258
|
+
raise PyFlowError("母线 %s %g kV 不存在" % (name, kv))
|
|
1259
|
+
result = self.set_load(name, kv, pload=p_mw, qload=q_mvar)
|
|
1260
|
+
result["semantics"] = "aggregate_bus_load"
|
|
1261
|
+
return result
|
|
1262
|
+
|
|
1263
|
+
def get_branch_status(self, bus1, kv1, bus2, kv2, ckt=None,
|
|
1264
|
+
section=None):
|
|
1265
|
+
"""返回支路当前是否在运,以及是否可由本实例快照恢复。
|
|
1266
|
+
|
|
1267
|
+
支路在内核支路表中出现时为在运;否则检查本实例 outage_branch
|
|
1268
|
+
保存的快照。基础算例中已经停运、从未进入内核支路表的支路无法
|
|
1269
|
+
通过该接口识别。
|
|
1270
|
+
"""
|
|
1271
|
+
def same(br, a, akv, b, bkv):
|
|
1272
|
+
return (br["bus1_name"].strip().upper() == a.strip().upper()
|
|
1273
|
+
and abs(br["kv1"] - akv) < 0.005
|
|
1274
|
+
and br["bus2_name"].strip().upper() == b.strip().upper()
|
|
1275
|
+
and abs(br["kv2"] - bkv) < 0.005
|
|
1276
|
+
and (ckt is None or br["ckt"] == ckt)
|
|
1277
|
+
and (section is None
|
|
1278
|
+
or int(br.get("section", 0)) == int(section)))
|
|
1279
|
+
|
|
1280
|
+
active = next((br for br in self.branches(input_only=True)
|
|
1281
|
+
if same(br, bus1, kv1, bus2, kv2)
|
|
1282
|
+
or same(br, bus2, kv2, bus1, kv1)), None)
|
|
1283
|
+
snap = None
|
|
1284
|
+
for candidate in self._branch_snapshots.values():
|
|
1285
|
+
if (same(candidate, bus1, kv1, bus2, kv2)
|
|
1286
|
+
or same(candidate, bus2, kv2, bus1, kv1)):
|
|
1287
|
+
snap = candidate
|
|
1288
|
+
break
|
|
1289
|
+
if active is not None:
|
|
1290
|
+
return {"in_service": True, "branch": active,
|
|
1291
|
+
"status": "IN", "restorable": snap is not None}
|
|
1292
|
+
if snap is not None:
|
|
1293
|
+
out = dict(snap)
|
|
1294
|
+
out.update({"in_service": False, "status": "OUT",
|
|
1295
|
+
"solution_available": False})
|
|
1296
|
+
out.update(_branch_loading(out))
|
|
1297
|
+
return {"in_service": False, "branch": out,
|
|
1298
|
+
"status": "OUT", "restorable": True}
|
|
1299
|
+
return {"in_service": False, "branch": None,
|
|
1300
|
+
"status": "UNKNOWN", "restorable": False}
|
|
1301
|
+
|
|
1302
|
+
def set_branch_status(self, bus1, kv1, bus2, kv2, ckt=None,
|
|
1303
|
+
in_service=True, section=None):
|
|
1304
|
+
"""幂等设置线路投切状态,并返回设置后的状态。"""
|
|
1305
|
+
state = self.get_branch_status(bus1, kv1, bus2, kv2, ckt,
|
|
1306
|
+
section=section)
|
|
1307
|
+
wanted = bool(in_service)
|
|
1308
|
+
if state["in_service"] == wanted:
|
|
1309
|
+
state.update({"changed": False, "result": None})
|
|
1310
|
+
return state
|
|
1311
|
+
if wanted:
|
|
1312
|
+
result = self.restore_branch(bus1, kv1, bus2, kv2, ckt=ckt,
|
|
1313
|
+
section=section)
|
|
1314
|
+
else:
|
|
1315
|
+
result = self.outage_branch(bus1, kv1, bus2, kv2, ckt=ckt,
|
|
1316
|
+
section=section)
|
|
1317
|
+
state = self.get_branch_status(bus1, kv1, bus2, kv2, ckt,
|
|
1318
|
+
section=section)
|
|
1319
|
+
state.update({"changed": True, "result": result})
|
|
1320
|
+
if state["in_service"] != wanted:
|
|
1321
|
+
raise PyFlowError("支路状态设置后读回不一致")
|
|
1322
|
+
return state
|
|
1323
|
+
|
|
1324
|
+
def delete_bus(self, name, kv):
|
|
1325
|
+
"""删除母线并提交(相连支路随之删除)。"""
|
|
1326
|
+
return self._one_shot("delete_bus", name, kv)
|
|
1327
|
+
|
|
1328
|
+
def outage_branch(self, bus1, kv1, bus2, kv2, ckt=None,
|
|
1329
|
+
section=None):
|
|
1330
|
+
"""支路开断并提交(需参数供恢复时使用)。"""
|
|
1331
|
+
return self._one_shot("outage_branch", bus1, kv1, bus2, kv2,
|
|
1332
|
+
ckt=ckt, section=section)
|
|
1333
|
+
|
|
1334
|
+
def restore_branch(self, bus1, kv1, bus2, kv2, ckt=None,
|
|
1335
|
+
section=None, **fields):
|
|
1336
|
+
"""恢复此前开断的支路并提交(参数取自开断快照)。"""
|
|
1337
|
+
return self._one_shot("restore_branch", bus1, kv1, bus2, kv2,
|
|
1338
|
+
ckt=ckt, section=section, **fields)
|
|
1339
|
+
|
|
1340
|
+
def change_tap(self, bus1, kv1, bus2, kv2, ckt=None, tap1=None,
|
|
1341
|
+
tap2=None):
|
|
1342
|
+
"""修改变压器变比并提交。"""
|
|
1343
|
+
return self._one_shot("change_tap", bus1, kv1, bus2, kv2, ckt=ckt,
|
|
1344
|
+
tap1=tap1, tap2=tap2)
|
|
1345
|
+
|
|
1346
|
+
def change_branch(self, bus1, kv1, bus2, kv2, ckt=None, **fields):
|
|
1347
|
+
"""通用支路参数修改并提交(字段见 cards.branch_card)。"""
|
|
1348
|
+
return self._one_shot("change_branch", bus1, kv1, bus2, kv2,
|
|
1349
|
+
ckt=ckt, **fields)
|
|
1350
|
+
|
|
1351
|
+
def add_branch(self, bus1, kv1, bus2, kv2, ckt="1", **fields):
|
|
1352
|
+
"""新增支路并提交。"""
|
|
1353
|
+
return self._one_shot("add_branch", bus1, kv1, bus2, kv2, ckt=ckt,
|
|
1354
|
+
**fields)
|
|
1355
|
+
|
|
1356
|
+
def scale_load(self, pct=1.0, zone=None, owner=None, area=None):
|
|
1357
|
+
"""按分区/所有者/全网缩放负荷并提交(pct=1.1 即 +10%)。"""
|
|
1358
|
+
return self._one_shot("scale_load", pct=pct, zone=zone,
|
|
1359
|
+
owner=owner, area=area)
|
|
1360
|
+
|
|
1361
|
+
def scale_gen(self, pct=1.0, zone=None, owner=None):
|
|
1362
|
+
"""按分区/所有者缩放出力并提交。"""
|
|
1363
|
+
return self._one_shot("scale_gen", pct=pct, zone=zone,
|
|
1364
|
+
owner=owner)
|
|
1365
|
+
|
|
1366
|
+
# ------------------------------------------------------------------
|
|
1367
|
+
# ctypes 原型
|
|
1368
|
+
# ------------------------------------------------------------------
|
|
1369
|
+
def _setup_prototypes(self):
|
|
1370
|
+
L = self._lib
|
|
1371
|
+
|
|
1372
|
+
# 基础 pf_* API(libcflow 直调层)
|
|
1373
|
+
L.pf_init.argtypes = []
|
|
1374
|
+
L.pf_init.restype = c_int
|
|
1375
|
+
L.pf_solution.argtypes = []
|
|
1376
|
+
L.pf_solution.restype = c_int
|
|
1377
|
+
L.pf_load_oldbase.argtypes = [c_char_p]
|
|
1378
|
+
L.pf_load_oldbase.restype = c_int
|
|
1379
|
+
L.pf_load_netdata.argtypes = [c_char_p]
|
|
1380
|
+
L.pf_load_netdata.restype = c_int
|
|
1381
|
+
L.pf_load_changes.argtypes = [c_char_p]
|
|
1382
|
+
L.pf_load_changes.restype = c_int
|
|
1383
|
+
L.pf_load_refbase.argtypes = [c_char_p]
|
|
1384
|
+
L.pf_load_refbase.restype = c_int
|
|
1385
|
+
L.pf_save_newbase.argtypes = [c_char_p]
|
|
1386
|
+
L.pf_save_newbase.restype = c_int
|
|
1387
|
+
L.pf_save_changes.argtypes = [c_char_p]
|
|
1388
|
+
L.pf_save_changes.restype = c_int
|
|
1389
|
+
L.pf_put_inrec.argtypes = [c_char_p]
|
|
1390
|
+
L.pf_put_inrec.restype = c_int
|
|
1391
|
+
L.pf_command.argtypes = [c_char_p]
|
|
1392
|
+
L.pf_command.restype = c_int
|
|
1393
|
+
L.pf_select_base.argtypes = [c_char]
|
|
1394
|
+
L.pf_select_base.restype = c_int
|
|
1395
|
+
L.pf_solve_area.argtypes = [c_char]
|
|
1396
|
+
L.pf_solve_area.restype = c_int
|
|
1397
|
+
L.pf_bus_exists.argtypes = [c_char_p, c_float]
|
|
1398
|
+
L.pf_bus_exists.restype = c_int
|
|
1399
|
+
L.pf_get_list.argtypes = [c_char_p, c_int, c_int, c_char_p]
|
|
1400
|
+
L.pf_get_list.restype = c_int
|
|
1401
|
+
L.pf_area_of_zone.argtypes = [c_char_p, c_char_p]
|
|
1402
|
+
L.pf_area_of_zone.restype = c_int
|
|
1403
|
+
L.pf_rename_bus.argtypes = [c_char_p, c_float, c_char_p, c_float]
|
|
1404
|
+
L.pf_rename_bus.restype = c_int
|
|
1405
|
+
L.pf_rename_area.argtypes = [c_char_p, c_char_p]
|
|
1406
|
+
L.pf_rename_area.restype = c_int
|
|
1407
|
+
L.pf_rename_zone.argtypes = [c_char_p, c_char_p]
|
|
1408
|
+
L.pf_rename_zone.restype = c_int
|
|
1409
|
+
L.pf_del_area.argtypes = [c_char_p]
|
|
1410
|
+
L.pf_del_area.restype = c_int
|
|
1411
|
+
L.pf_del_zone.argtypes = [c_char_p]
|
|
1412
|
+
L.pf_del_zone.restype = c_int
|
|
1413
|
+
L.pf_save_netdata.argtypes = [c_char_p, c_char_p, c_char_p, c_int]
|
|
1414
|
+
L.pf_save_netdata.restype = c_int
|
|
1415
|
+
L.pf_save_wscc_stab_data.argtypes = [c_char_p, c_char_p]
|
|
1416
|
+
L.pf_save_wscc_stab_data.restype = c_int
|
|
1417
|
+
# 工具箱直调(ipfpy_tools.c:puf / ipfcut 的替代入口)。
|
|
1418
|
+
L.bpaflow_puf.argtypes = [c_int, ctypes.POINTER(c_char_p)]
|
|
1419
|
+
L.bpaflow_puf.restype = c_int
|
|
1420
|
+
L.bpaflow_ipfcut.argtypes = [c_char_p, c_char_p, c_char_p]
|
|
1421
|
+
L.bpaflow_ipfcut.restype = c_int
|
|
1422
|
+
# 收尾:只调 pflend_(打印运行时间并关闭引擎打开的文件单元)。
|
|
1423
|
+
# 不可调 ipf_fexit_cw —— 其内部 call exit 会终止宿主 Python 进程。
|
|
1424
|
+
L.pflend_.argtypes = []
|
|
1425
|
+
L.pflend_.restype = None
|
|
1426
|
+
|
|
1427
|
+
# ---- 内部数据访问(ipfpy_api.c)----
|
|
1428
|
+
L.ipfpy_y_info.argtypes = [ctypes.POINTER(c_int)] * 3
|
|
1429
|
+
L.ipfpy_y_info.restype = None
|
|
1430
|
+
L.ipfpy_y_data.argtypes = [
|
|
1431
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1432
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_double),
|
|
1433
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_double),
|
|
1434
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_int),
|
|
1435
|
+
ctypes.POINTER(c_int)]
|
|
1436
|
+
L.ipfpy_y_data.restype = None
|
|
1437
|
+
L.ipfpy_mismatch.argtypes = (
|
|
1438
|
+
[ctypes.POINTER(c_double)] * 5 +
|
|
1439
|
+
[ctypes.POINTER(c_int)] * 10)
|
|
1440
|
+
L.ipfpy_mismatch.restype = None
|
|
1441
|
+
L.ipfpy_errbus.argtypes = [
|
|
1442
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_int)]
|
|
1443
|
+
L.ipfpy_errbus.restype = None
|
|
1444
|
+
L.ipfpy_jac_data.argtypes = [
|
|
1445
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_double),
|
|
1446
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_int),
|
|
1447
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int)]
|
|
1448
|
+
L.ipfpy_jac_data.restype = None
|
|
1449
|
+
L.ipfpy_voltage.argtypes = [
|
|
1450
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_double),
|
|
1451
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_int),
|
|
1452
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1453
|
+
ctypes.POINTER(c_int)]
|
|
1454
|
+
L.ipfpy_voltage.restype = None
|
|
1455
|
+
L.ipfpy_busnames.argtypes = [
|
|
1456
|
+
ctypes.POINTER(c_int),
|
|
1457
|
+
ctypes.POINTER(c_char), ctypes.POINTER(c_char),
|
|
1458
|
+
ctypes.POINTER(c_char)]
|
|
1459
|
+
L.ipfpy_busnames.restype = None
|
|
1460
|
+
L.ipfpy_get_ltc.argtypes = [
|
|
1461
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1462
|
+
ctypes.POINTER(c_char), ctypes.POINTER(c_char),
|
|
1463
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_float),
|
|
1464
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_double),
|
|
1465
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_float),
|
|
1466
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_int),
|
|
1467
|
+
ctypes.POINTER(c_int)]
|
|
1468
|
+
L.ipfpy_get_ltc.restype = None
|
|
1469
|
+
L.ipfpy_mismatch_bus.argtypes = [
|
|
1470
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_char),
|
|
1471
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_double),
|
|
1472
|
+
ctypes.POINTER(c_float)]
|
|
1473
|
+
L.ipfpy_mismatch_bus.restype = None
|
|
1474
|
+
L.ipfpy_step_state.argtypes = [
|
|
1475
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1476
|
+
ctypes.POINTER(c_int)]
|
|
1477
|
+
L.ipfpy_step_state.restype = None
|
|
1478
|
+
L.ipfpy_step_reset.argtypes = []
|
|
1479
|
+
L.ipfpy_step_reset.restype = None
|
|
1480
|
+
L.ipfpy_iteration_info.argtypes = (
|
|
1481
|
+
[ctypes.POINTER(c_int)] * 7 + [ctypes.POINTER(c_float)])
|
|
1482
|
+
L.ipfpy_iteration_info.restype = None
|
|
1483
|
+
L.ipfpy_iteration_bus.argtypes = (
|
|
1484
|
+
[ctypes.POINTER(c_int)] + [ctypes.POINTER(c_double)] * 6
|
|
1485
|
+
+ [ctypes.POINTER(c_int)] * 3)
|
|
1486
|
+
L.ipfpy_iteration_bus.restype = None
|
|
1487
|
+
L.ipfpy_iteration_jac.argtypes = (
|
|
1488
|
+
[ctypes.POINTER(c_int)] * 4
|
|
1489
|
+
+ [ctypes.POINTER(c_double)] * 6)
|
|
1490
|
+
L.ipfpy_iteration_jac.restype = None
|
|
1491
|
+
L.ipfpy_case_info.argtypes = [
|
|
1492
|
+
ctypes.POINTER(c_float),
|
|
1493
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1494
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1495
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1496
|
+
ctypes.POINTER(c_char)]
|
|
1497
|
+
L.ipfpy_case_info.restype = None
|
|
1498
|
+
L.ipfpy_case_info_ext.argtypes = [
|
|
1499
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_int),
|
|
1500
|
+
ctypes.POINTER(c_char)]
|
|
1501
|
+
L.ipfpy_case_info_ext.restype = None
|
|
1502
|
+
L.ipfpy_get_buses.argtypes = [
|
|
1503
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_int]
|
|
1504
|
+
L.ipfpy_get_buses.restype = c_int
|
|
1505
|
+
L.ipfpy_get_branches.argtypes = [
|
|
1506
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1507
|
+
c_void_p, c_void_p, c_int]
|
|
1508
|
+
L.ipfpy_get_branches.restype = c_int
|
|
1509
|
+
L.ipfpy_get_branches_input.argtypes = [
|
|
1510
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1511
|
+
c_void_p, c_void_p, c_int]
|
|
1512
|
+
L.ipfpy_get_branches_input.restype = c_int
|
|
1513
|
+
L.ipfpy_get_areas.argtypes = [
|
|
1514
|
+
c_void_p, c_void_p, c_void_p, c_int]
|
|
1515
|
+
L.ipfpy_get_areas.restype = c_int
|
|
1516
|
+
|
|
1517
|
+
# ---- 扩展导出(新 DLL 才提供;旧 DLL 下方法自动回退)----
|
|
1518
|
+
if hasattr(L, "ipfpy_get_branches_ext"):
|
|
1519
|
+
L.ipfpy_get_branches_ext.argtypes = [
|
|
1520
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1521
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1522
|
+
c_int, c_int]
|
|
1523
|
+
L.ipfpy_get_branches_ext.restype = c_int
|
|
1524
|
+
if hasattr(L, "ipfpy_get_ltc_ext"):
|
|
1525
|
+
L.ipfpy_get_ltc_ext.argtypes = [
|
|
1526
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_int),
|
|
1527
|
+
ctypes.POINTER(c_char), ctypes.POINTER(c_char),
|
|
1528
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_float),
|
|
1529
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_double),
|
|
1530
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_float),
|
|
1531
|
+
ctypes.POINTER(c_float), ctypes.POINTER(c_int),
|
|
1532
|
+
ctypes.POINTER(c_int), ctypes.POINTER(c_double),
|
|
1533
|
+
ctypes.POINTER(c_double), ctypes.POINTER(c_int),
|
|
1534
|
+
ctypes.POINTER(c_int)]
|
|
1535
|
+
L.ipfpy_get_ltc_ext.restype = None
|
|
1536
|
+
if hasattr(L, "ipfpy_get_ities"):
|
|
1537
|
+
L.ipfpy_get_ities.argtypes = [
|
|
1538
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_int]
|
|
1539
|
+
L.ipfpy_get_ities.restype = c_int
|
|
1540
|
+
if hasattr(L, "ipfpy_get_cbuses"):
|
|
1541
|
+
L.ipfpy_get_cbuses.argtypes = [
|
|
1542
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1543
|
+
c_void_p, c_int]
|
|
1544
|
+
L.ipfpy_get_cbuses.restype = c_int
|
|
1545
|
+
if hasattr(L, "ipfpy_get_qcurves"):
|
|
1546
|
+
L.ipfpy_get_qcurves.argtypes = [
|
|
1547
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1548
|
+
c_void_p, c_int]
|
|
1549
|
+
L.ipfpy_get_qcurves.restype = c_int
|
|
1550
|
+
if hasattr(L, "ipfpy_get_xdata"):
|
|
1551
|
+
L.ipfpy_get_xdata.argtypes = [
|
|
1552
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
|
|
1553
|
+
c_void_p, c_void_p, c_int]
|
|
1554
|
+
L.ipfpy_get_xdata.restype = c_int
|
|
1555
|
+
if hasattr(L, "ipfpy_get_comments"):
|
|
1556
|
+
L.ipfpy_get_comments.argtypes = [
|
|
1557
|
+
c_void_p, c_void_p, c_void_p, c_void_p]
|
|
1558
|
+
L.ipfpy_get_comments.restype = c_int
|
|
1559
|
+
|
|
1560
|
+
# ------------------------------------------------------------------
|
|
1561
|
+
# 基础 pf_* API
|
|
1562
|
+
# ------------------------------------------------------------------
|
|
1563
|
+
def init(self):
|
|
1564
|
+
"""初始化直调引擎。
|
|
1565
|
+
|
|
1566
|
+
先调 cflow_pf_init()(initlz+pfinit:设置逻辑单元号、打开单元 13
|
|
1567
|
+
临时输入文件等),再发 /initialize 命令(等价 pf_init)。
|
|
1568
|
+
"""
|
|
1569
|
+
if self._closed:
|
|
1570
|
+
raise PyFlowError("BPAFlow 实例已关闭;请创建新实例")
|
|
1571
|
+
if self._initialized:
|
|
1572
|
+
return 0
|
|
1573
|
+
if not hasattr(self._lib, "ipfpy_set_embedded"):
|
|
1574
|
+
raise PyFlowError(
|
|
1575
|
+
"libipfpy 过旧,缺少 ipfpy_set_embedded;请重建 libipfpy "
|
|
1576
|
+
"或设置 BPAFLOW_DLL 指向当前构建")
|
|
1577
|
+
self._lib.ipfpy_set_embedded.argtypes = [ctypes.POINTER(c_int)]
|
|
1578
|
+
self._lib.ipfpy_set_embedded.restype = None
|
|
1579
|
+
embedded_flag = c_int(1)
|
|
1580
|
+
self._lib.ipfpy_set_embedded(byref(embedded_flag))
|
|
1581
|
+
self._lib.cflow_pf_init.argtypes = []
|
|
1582
|
+
self._lib.cflow_pf_init.restype = None
|
|
1583
|
+
self._lib.cflow_pf_init()
|
|
1584
|
+
self._initialized = True
|
|
1585
|
+
return self._lib.pf_init()
|
|
1586
|
+
|
|
1587
|
+
def close(self):
|
|
1588
|
+
"""收尾:打印运行时间并关闭引擎打开的文件单元(等价 pflend)。
|
|
1589
|
+
|
|
1590
|
+
注意不可调 ipf_fexit_cw(其内部 call exit 会终止宿主 Python
|
|
1591
|
+
进程)。不调用 close() 也无妨 —— 进程退出时由 OS 回收。
|
|
1592
|
+
"""
|
|
1593
|
+
if self._closed:
|
|
1594
|
+
return
|
|
1595
|
+
if self._initialized:
|
|
1596
|
+
self._lib.pflend_()
|
|
1597
|
+
self._invalidate_cache()
|
|
1598
|
+
self.drop_snapshot()
|
|
1599
|
+
self._closed = True
|
|
1600
|
+
if self._audit is not None and self._audit_owned:
|
|
1601
|
+
try:
|
|
1602
|
+
self._audit.close()
|
|
1603
|
+
except (OSError, ValueError):
|
|
1604
|
+
pass
|
|
1605
|
+
|
|
1606
|
+
exit = close
|
|
1607
|
+
|
|
1608
|
+
def __enter__(self):
|
|
1609
|
+
if not self._initialized:
|
|
1610
|
+
self.init()
|
|
1611
|
+
return self
|
|
1612
|
+
|
|
1613
|
+
def __exit__(self, exc_type, exc, tb):
|
|
1614
|
+
self.close()
|
|
1615
|
+
return False
|
|
1616
|
+
|
|
1617
|
+
def _ensure_mutation_allowed(self):
|
|
1618
|
+
"""防止在线性化点与网络模型之间制造不一致。"""
|
|
1619
|
+
if self._solution_session == "ITERATING":
|
|
1620
|
+
raise PyFlowError(
|
|
1621
|
+
"单步潮流会话正在迭代;修改网络前请先调用 "
|
|
1622
|
+
"abort_solution(),修改后再 start_solution()")
|
|
1623
|
+
|
|
1624
|
+
def configure_solution(self, decoupled=None, newton=None, optim=None,
|
|
1625
|
+
flat_start=None):
|
|
1626
|
+
"""设置并记录潮流计算参数,返回当前 API 配置快照。"""
|
|
1627
|
+
if any(v is not None for v in (decoupled, newton, optim)):
|
|
1628
|
+
rc = self.set_solution_iterations(decoupled=decoupled,
|
|
1629
|
+
newton=newton, optim=optim)
|
|
1630
|
+
if rc != 0:
|
|
1631
|
+
raise PyFlowError("设置潮流迭代次数失败,rc=%s" % rc)
|
|
1632
|
+
if flat_start is not None:
|
|
1633
|
+
rc = self.set_misc_controls(vflatstart=bool(flat_start))
|
|
1634
|
+
if rc != 0:
|
|
1635
|
+
raise PyFlowError("设置平启动参数失败,rc=%s" % rc)
|
|
1636
|
+
return self.get_solution_config()
|
|
1637
|
+
|
|
1638
|
+
def get_solution_config(self):
|
|
1639
|
+
"""返回本实例经公共 API 设置的有效潮流参数。"""
|
|
1640
|
+
return dict(self._solution_config)
|
|
1641
|
+
|
|
1642
|
+
def start_solution(self, max_iterations=None, flat_start=None):
|
|
1643
|
+
"""启动严格 Newton 单步会话,但不执行迭代。
|
|
1644
|
+
|
|
1645
|
+
单步模式强制跳过解耦法和最优乘子预迭代,保证每次
|
|
1646
|
+
step_solution() 只推进一次 Newton 线性化/回代/更新。
|
|
1647
|
+
"""
|
|
1648
|
+
if max_iterations is None:
|
|
1649
|
+
max_iterations = self._solution_config.get("newton") or 30
|
|
1650
|
+
if int(max_iterations) <= 0:
|
|
1651
|
+
raise ValueError("max_iterations 必须为正整数")
|
|
1652
|
+
self._lib.ipfpy_step_reset()
|
|
1653
|
+
self.configure_solution(decoupled=0, newton=int(max_iterations),
|
|
1654
|
+
optim=0, flat_start=flat_start)
|
|
1655
|
+
self._solution_session = "READY"
|
|
1656
|
+
return self.solution_status()
|
|
1657
|
+
|
|
1658
|
+
def solution_status(self):
|
|
1659
|
+
"""返回单步会话状态:IDLE/READY/ITERATING/CONVERGED/FAILED。"""
|
|
1660
|
+
active, iteration, engine_state = c_int(), c_int(), c_int()
|
|
1661
|
+
self._lib.ipfpy_step_state(byref(active), byref(iteration),
|
|
1662
|
+
byref(engine_state))
|
|
1663
|
+
return {
|
|
1664
|
+
"status": self._solution_session,
|
|
1665
|
+
"iteration": iteration.value,
|
|
1666
|
+
"engine_active": bool(active.value),
|
|
1667
|
+
"engine_state": engine_state.value,
|
|
1668
|
+
"converged": self._solution_session == "CONVERGED",
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
def abort_solution(self):
|
|
1672
|
+
"""终止并清空当前单步会话;网络状态保留在当前迭代点。"""
|
|
1673
|
+
self._lib.ipfpy_step_reset()
|
|
1674
|
+
self._solution_session = "IDLE"
|
|
1675
|
+
return self.solution_status()
|
|
1676
|
+
|
|
1677
|
+
reset_solution = abort_solution
|
|
1678
|
+
|
|
1679
|
+
def solution(self):
|
|
1680
|
+
"""完整求解当前案例(直接调用 pf_solution)。"""
|
|
1681
|
+
self._lib.ipfpy_step_reset()
|
|
1682
|
+
self._solution_session = "IDLE"
|
|
1683
|
+
rc = self._lib.pf_solution()
|
|
1684
|
+
self._invalidate_cache()
|
|
1685
|
+
self._solution_session = "CONVERGED" if rc == 0 else "FAILED"
|
|
1686
|
+
return rc
|
|
1687
|
+
|
|
1688
|
+
def step_solution(self):
|
|
1689
|
+
"""执行单步潮流迭代。
|
|
1690
|
+
|
|
1691
|
+
每次调用执行一次 NR 迭代,保留中间电压/偏差状态,
|
|
1692
|
+
可在迭代间通过 voltages()/mismatch()/iteration_history()
|
|
1693
|
+
读取中间结果。
|
|
1694
|
+
|
|
1695
|
+
首次调用会完成 Y 矩阵与状态初始化并执行第 1 次 NR 迭代;
|
|
1696
|
+
后续调用从保留的中间状态继续执行恰好一次迭代。
|
|
1697
|
+
|
|
1698
|
+
Returns
|
|
1699
|
+
-------
|
|
1700
|
+
dict
|
|
1701
|
+
``status`` 为 ``in_progress``、``converged`` 或 ``failed``,
|
|
1702
|
+
同时返回 ``rc``、``iteration``、``mismatch`` 和
|
|
1703
|
+
``voltage_summary``。
|
|
1704
|
+
"""
|
|
1705
|
+
if self._solution_session in ("IDLE", "CONVERGED", "FAILED"):
|
|
1706
|
+
self.start_solution()
|
|
1707
|
+
self.command("/ STEP_SOLUTION = 1")
|
|
1708
|
+
rc = self._lib.pf_solution()
|
|
1709
|
+
self._invalidate_cache()
|
|
1710
|
+
active, iteration, state = c_int(), c_int(), c_int()
|
|
1711
|
+
self._lib.ipfpy_step_state(byref(active), byref(iteration),
|
|
1712
|
+
byref(state))
|
|
1713
|
+
if active.value:
|
|
1714
|
+
status = "in_progress"
|
|
1715
|
+
self._solution_session = "ITERATING"
|
|
1716
|
+
elif state.value == 1:
|
|
1717
|
+
status = "converged"
|
|
1718
|
+
self._solution_session = "CONVERGED"
|
|
1719
|
+
else:
|
|
1720
|
+
status = "failed"
|
|
1721
|
+
self._solution_session = "FAILED"
|
|
1722
|
+
r = {"rc": rc, "status": status,
|
|
1723
|
+
"converged": status == "converged",
|
|
1724
|
+
"iteration": iteration.value,
|
|
1725
|
+
"mismatch": self.mismatch()}
|
|
1726
|
+
v = self.voltages()
|
|
1727
|
+
if v["nbus"] > 0:
|
|
1728
|
+
vm = [(v["e"][i] ** 2 + v["f"][i] ** 2) ** 0.5 * v["base"][i]
|
|
1729
|
+
for i in range(v["nbus"])]
|
|
1730
|
+
r["voltage_summary"] = {
|
|
1731
|
+
"vmin_kv": min(vm), "vmax_kv": max(vm),
|
|
1732
|
+
"vmin_bus": v["names"][vm.index(min(vm))].strip(),
|
|
1733
|
+
"vmax_bus": v["names"][vm.index(max(vm))].strip(),
|
|
1734
|
+
}
|
|
1735
|
+
return r
|
|
1736
|
+
|
|
1737
|
+
def solution_step(self):
|
|
1738
|
+
"""step_solution() 的向后兼容别名。"""
|
|
1739
|
+
return self.step_solution()
|
|
1740
|
+
|
|
1741
|
+
def load_oldbase(self, filename):
|
|
1742
|
+
"""加载潮流数据文件(BSE/DAT,等价 pf_load_oldbase)。"""
|
|
1743
|
+
self.abort_solution()
|
|
1744
|
+
self._branch_snapshots.clear()
|
|
1745
|
+
self._invalidate_cache()
|
|
1746
|
+
return self._lib.pf_load_oldbase(_ascii(filename))
|
|
1747
|
+
|
|
1748
|
+
def load_netdata(self, filename):
|
|
1749
|
+
self.abort_solution()
|
|
1750
|
+
self._branch_snapshots.clear()
|
|
1751
|
+
self._invalidate_cache()
|
|
1752
|
+
return self._lib.pf_load_netdata(_ascii(filename))
|
|
1753
|
+
|
|
1754
|
+
def load_changes(self, filename):
|
|
1755
|
+
self._ensure_mutation_allowed()
|
|
1756
|
+
self._invalidate_cache()
|
|
1757
|
+
return self._lib.pf_load_changes(_ascii(filename))
|
|
1758
|
+
|
|
1759
|
+
def load_refbase(self, filename):
|
|
1760
|
+
self._invalidate_cache()
|
|
1761
|
+
return self._lib.pf_load_refbase(_ascii(filename))
|
|
1762
|
+
|
|
1763
|
+
def save_newbase(self, filename):
|
|
1764
|
+
return self._lib.pf_save_newbase(_ascii(filename))
|
|
1765
|
+
|
|
1766
|
+
def save_changes(self, filename):
|
|
1767
|
+
return self._lib.pf_save_changes(_ascii(filename))
|
|
1768
|
+
|
|
1769
|
+
def put_inrec(self, record):
|
|
1770
|
+
self._ensure_mutation_allowed()
|
|
1771
|
+
self._invalidate_cache()
|
|
1772
|
+
return self._lib.pf_put_inrec(_ascii(record))
|
|
1773
|
+
|
|
1774
|
+
def command(self, command):
|
|
1775
|
+
self._invalidate_cache()
|
|
1776
|
+
return self._lib.pf_command(_ascii(command))
|
|
1777
|
+
|
|
1778
|
+
def run_command_file(self, pcl_file):
|
|
1779
|
+
"""执行 PCL 命令文件,等价原 ipfbat 批处理入口。
|
|
1780
|
+
|
|
1781
|
+
经 /COMMAND 通道由引擎解析执行:文件中的 PCL 命令按顺序送入
|
|
1782
|
+
srv_cmdprs,支持嵌套命令文件;任一命令返回非零即中止并返回该状态。
|
|
1783
|
+
取代已移除的 ipfbat 独立程序。
|
|
1784
|
+
|
|
1785
|
+
Parameters
|
|
1786
|
+
----------
|
|
1787
|
+
pcl_file : str
|
|
1788
|
+
PCL 命令文件路径(相对路径按当前工作目录解析)。
|
|
1789
|
+
|
|
1790
|
+
Returns
|
|
1791
|
+
-------
|
|
1792
|
+
int
|
|
1793
|
+
0 成功,非零为失败命令的状态码。
|
|
1794
|
+
"""
|
|
1795
|
+
return self.command("/COMMAND, FILE=%s" % pcl_file)
|
|
1796
|
+
|
|
1797
|
+
# ------------------------------------------------------------------
|
|
1798
|
+
# .pfc 批处理文件解析与执行
|
|
1799
|
+
def load_pfc(self, pfc_path):
|
|
1800
|
+
"""加载并执行 .pfc 批处理控制文件中的控制命令。
|
|
1801
|
+
|
|
1802
|
+
解析 .pfc 文件中的控制命令(/ 和 . 开头行),按顺序通过
|
|
1803
|
+
pf_command() 发送给引擎。数据卡(B/L/T/E/R/P 等)会被跳过,
|
|
1804
|
+
因为它们由 / NEW_BASE 或 / OLD_BASE 引用的 BSE 文件提供。
|
|
1805
|
+
|
|
1806
|
+
注意: 不处理内联数据卡(B/L/T/E/R 等),仅处理控制命令。
|
|
1807
|
+
|
|
1808
|
+
Parameters
|
|
1809
|
+
----------
|
|
1810
|
+
pfc_path : str
|
|
1811
|
+
.pfc 文件路径
|
|
1812
|
+
|
|
1813
|
+
Returns
|
|
1814
|
+
-------
|
|
1815
|
+
int
|
|
1816
|
+
0 成功,非零失败
|
|
1817
|
+
"""
|
|
1818
|
+
pfc_dir = os.path.dirname(os.path.abspath(pfc_path))
|
|
1819
|
+
with open(pfc_path, encoding="ascii", errors="replace") as f:
|
|
1820
|
+
raw = f.read()
|
|
1821
|
+
|
|
1822
|
+
# 预处理:移除续行符(行尾反斜杠,后接可选空格),保留换行
|
|
1823
|
+
import re
|
|
1824
|
+
raw = re.sub(r'\\(\s*[\r\n]+)', r'\1', raw)
|
|
1825
|
+
lines = raw.splitlines()
|
|
1826
|
+
|
|
1827
|
+
# 执行所有控制命令(/ 和 . 开头)
|
|
1828
|
+
for raw_line in lines:
|
|
1829
|
+
stripped = raw_line.strip()
|
|
1830
|
+
if not stripped or stripped.startswith("C") or stripped.startswith("("):
|
|
1831
|
+
continue
|
|
1832
|
+
if not (stripped.startswith("/") or stripped.startswith(".")):
|
|
1833
|
+
continue # 跳过数据卡
|
|
1834
|
+
|
|
1835
|
+
upper = stripped.upper()
|
|
1836
|
+
# 处理 NEW_BASE/OLD_BASE 命令
|
|
1837
|
+
if upper.startswith("/ NEW_BASE") or upper.startswith("/NEW_BASE"):
|
|
1838
|
+
fname = _pfc_filename(stripped)
|
|
1839
|
+
if fname:
|
|
1840
|
+
# 尝试相对路径
|
|
1841
|
+
fpath = fname if os.path.isabs(fname) else \
|
|
1842
|
+
os.path.join(pfc_dir, fname)
|
|
1843
|
+
rc = self.load_oldbase(fpath)
|
|
1844
|
+
if rc != 0:
|
|
1845
|
+
# 尝试原始文件名(可能全大写)
|
|
1846
|
+
import glob
|
|
1847
|
+
dir_files = os.listdir(pfc_dir)
|
|
1848
|
+
match = [f for f in dir_files if
|
|
1849
|
+
f.upper() == fname.upper()]
|
|
1850
|
+
if match:
|
|
1851
|
+
rc = self.load_oldbase(
|
|
1852
|
+
os.path.join(pfc_dir, match[0]))
|
|
1853
|
+
if rc != 0:
|
|
1854
|
+
return rc
|
|
1855
|
+
else:
|
|
1856
|
+
return rc
|
|
1857
|
+
continue
|
|
1858
|
+
if upper.startswith("/ OLD_BASE") or upper.startswith("/OLD_BASE"):
|
|
1859
|
+
fname = _pfc_filename(stripped)
|
|
1860
|
+
if fname:
|
|
1861
|
+
fpath = fname if os.path.isabs(fname) else \
|
|
1862
|
+
os.path.join(pfc_dir, fname)
|
|
1863
|
+
rc = self.load_oldbase(fpath)
|
|
1864
|
+
if rc != 0:
|
|
1865
|
+
return rc
|
|
1866
|
+
continue
|
|
1867
|
+
# 跳过 NETWORK_DATA(内联数据由 BSE 文件提供)
|
|
1868
|
+
if upper.startswith("/ NETWORK_DATA") or upper.startswith("/NETWORK_DATA"):
|
|
1869
|
+
continue
|
|
1870
|
+
|
|
1871
|
+
# 其他控制命令直接发送
|
|
1872
|
+
rc = self.command(stripped)
|
|
1873
|
+
if rc != 0:
|
|
1874
|
+
return rc
|
|
1875
|
+
|
|
1876
|
+
return 0
|
|
1877
|
+
|
|
1878
|
+
@staticmethod
|
|
1879
|
+
def parse_pfc(pfc_path):
|
|
1880
|
+
"""解析 .pfc 批处理控制文件,返回结构化命令列表。
|
|
1881
|
+
|
|
1882
|
+
返回每条控制命令的类别、原始文本和解析后的参数,不执行任何操作。
|
|
1883
|
+
|
|
1884
|
+
Parameters
|
|
1885
|
+
----------
|
|
1886
|
+
pfc_path : str
|
|
1887
|
+
.pfc 文件路径
|
|
1888
|
+
|
|
1889
|
+
Returns
|
|
1890
|
+
-------
|
|
1891
|
+
dict
|
|
1892
|
+
{"header": {...}, "controls": [{...}, ...], "data_cards": int}
|
|
1893
|
+
"""
|
|
1894
|
+
import re
|
|
1895
|
+
with open(pfc_path, encoding="ascii", errors="replace") as f:
|
|
1896
|
+
raw = f.read()
|
|
1897
|
+
raw = re.sub(r'\\(\s*[\r\n]+)', r'\1', raw)
|
|
1898
|
+
lines = raw.splitlines()
|
|
1899
|
+
|
|
1900
|
+
result = {"header": {}, "controls": [], "data_cards": 0}
|
|
1901
|
+
|
|
1902
|
+
for raw_line in lines:
|
|
1903
|
+
stripped = raw_line.strip()
|
|
1904
|
+
if not stripped or stripped.startswith("C"):
|
|
1905
|
+
continue
|
|
1906
|
+
|
|
1907
|
+
# 头部: (POWERFLOW, ...)
|
|
1908
|
+
if stripped.startswith("("):
|
|
1909
|
+
m = re.match(r'\(\s*POWERFLOW\s*,?\s*(.*?)\)', stripped,
|
|
1910
|
+
re.IGNORECASE)
|
|
1911
|
+
if m:
|
|
1912
|
+
params = m.group(1)
|
|
1913
|
+
for kv in re.finditer(r'(\w+)\s*=\s*([^,]+)', params):
|
|
1914
|
+
result["header"][kv.group(1).upper()] = \
|
|
1915
|
+
kv.group(2).strip()
|
|
1916
|
+
continue
|
|
1917
|
+
|
|
1918
|
+
# 控制命令: / 或 . 开头
|
|
1919
|
+
if stripped.startswith("/") or stripped.startswith("."):
|
|
1920
|
+
upper = stripped.upper()
|
|
1921
|
+
entry = {"raw": stripped, "type": "unknown", "params": {}}
|
|
1922
|
+
|
|
1923
|
+
if upper.startswith("./ P_INPUTLIST"):
|
|
1924
|
+
entry["type"] = "INPUT_LIST"
|
|
1925
|
+
m = re.search(r'FULL|NONE|ZONES', upper)
|
|
1926
|
+
entry["params"]["mode"] = m.group(0) if m else "FULL"
|
|
1927
|
+
elif upper.startswith("./ P_OUTPUTLIST"):
|
|
1928
|
+
entry["type"] = "OUTPUT_LIST"
|
|
1929
|
+
m = re.search(r'FULL|NONE|ZONES', upper)
|
|
1930
|
+
entry["params"]["mode"] = m.group(0) if m else "FULL"
|
|
1931
|
+
elif upper.startswith("/ P_ANALYSIS_RPT"):
|
|
1932
|
+
entry["type"] = "ANALYSIS_RPT"
|
|
1933
|
+
m = re.search(r'LEVEL\s*=\s*(\d+)', upper)
|
|
1934
|
+
if m:
|
|
1935
|
+
entry["params"]["level"] = int(m.group(1))
|
|
1936
|
+
elif (upper.startswith("/ NEW_BASE") or
|
|
1937
|
+
upper.startswith("/NEW_BASE") or
|
|
1938
|
+
upper.startswith("/ OLD_BASE") or
|
|
1939
|
+
upper.startswith("/OLD_BASE")):
|
|
1940
|
+
entry["type"] = "BASE"
|
|
1941
|
+
fname = _pfc_filename(stripped)
|
|
1942
|
+
if fname:
|
|
1943
|
+
entry["params"]["file"] = fname
|
|
1944
|
+
elif upper.startswith("/ NETWORK_DATA"):
|
|
1945
|
+
entry["type"] = "NETWORK_DATA"
|
|
1946
|
+
m = re.search(r'FILE\s*=\s*(\S+)', stripped,
|
|
1947
|
+
re.IGNORECASE)
|
|
1948
|
+
entry["params"]["file"] = m.group(1) if m else "*"
|
|
1949
|
+
elif upper.startswith("/ MISC_CNTRL"):
|
|
1950
|
+
entry["type"] = "MISC_CNTRL"
|
|
1951
|
+
for kv in re.finditer(r'(\w+)\s*=\s*(\S+)', stripped):
|
|
1952
|
+
entry["params"][kv.group(1).upper()] = \
|
|
1953
|
+
kv.group(2).strip()
|
|
1954
|
+
elif upper.startswith("/ LIMITS"):
|
|
1955
|
+
entry["type"] = "LIMITS"
|
|
1956
|
+
for kv in re.finditer(r'(\w+)\s*=\s*([\d.]+)', upper):
|
|
1957
|
+
entry["params"][kv.group(1).upper()] = \
|
|
1958
|
+
float(kv.group(2))
|
|
1959
|
+
elif upper.startswith("/ SOL_ITER"):
|
|
1960
|
+
entry["type"] = "SOL_ITER"
|
|
1961
|
+
for kv in re.finditer(r'(\w+)\s*=\s*(\d+)', upper):
|
|
1962
|
+
entry["params"][kv.group(1).upper()] = \
|
|
1963
|
+
int(kv.group(2))
|
|
1964
|
+
elif upper.startswith("/ TOLERANCE"):
|
|
1965
|
+
entry["type"] = "TOLERANCE"
|
|
1966
|
+
for kv in re.finditer(r'(\w+)\s*=\s*([\d.]+)', upper):
|
|
1967
|
+
entry["params"][kv.group(1).upper()] = \
|
|
1968
|
+
float(kv.group(2))
|
|
1969
|
+
elif upper.startswith("/ LTC"):
|
|
1970
|
+
entry["type"] = "LTC"
|
|
1971
|
+
m = re.search(r'=\s*(\S+)', stripped)
|
|
1972
|
+
if m:
|
|
1973
|
+
entry["params"]["mode"] = m.group(1).strip()
|
|
1974
|
+
elif upper.startswith("/ MVA_BASE"):
|
|
1975
|
+
entry["type"] = "MVA_BASE"
|
|
1976
|
+
m = re.search(r'=\s*([\d.]+)', stripped)
|
|
1977
|
+
if m:
|
|
1978
|
+
entry["params"]["mva"] = float(m.group(1))
|
|
1979
|
+
elif upper.startswith("/ AI_LIST"):
|
|
1980
|
+
entry["type"] = "AI_LIST"
|
|
1981
|
+
m = re.search(r'=\s*(\S+)', stripped)
|
|
1982
|
+
if m:
|
|
1983
|
+
entry["params"]["mode"] = m.group(1).strip()
|
|
1984
|
+
elif upper.startswith("/ OI_LIST"):
|
|
1985
|
+
entry["type"] = "OI_LIST"
|
|
1986
|
+
m = re.search(r'=\s*(\S+)', stripped)
|
|
1987
|
+
if m:
|
|
1988
|
+
entry["params"]["mode"] = m.group(1).strip()
|
|
1989
|
+
elif upper.startswith("/ RPT_SORT"):
|
|
1990
|
+
entry["type"] = "RPT_SORT"
|
|
1991
|
+
m = re.search(r'=\s*(\S+)', stripped)
|
|
1992
|
+
if m:
|
|
1993
|
+
entry["params"]["mode"] = m.group(1).strip()
|
|
1994
|
+
elif upper.startswith("/ EXTENDED_RATINGS"):
|
|
1995
|
+
entry["type"] = "EXTENDED_RATINGS"
|
|
1996
|
+
m = re.search(r'=\s*(\S+)', stripped)
|
|
1997
|
+
if m:
|
|
1998
|
+
entry["params"]["season"] = m.group(1).strip()
|
|
1999
|
+
|
|
2000
|
+
result["controls"].append(entry)
|
|
2001
|
+
continue
|
|
2002
|
+
|
|
2003
|
+
# 数据卡
|
|
2004
|
+
if stripped[0] in "BLTERPX":
|
|
2005
|
+
result["data_cards"] += 1
|
|
2006
|
+
|
|
2007
|
+
return result
|
|
2008
|
+
|
|
2009
|
+
# ------------------------------------------------------------------
|
|
2010
|
+
# 控制参数设置(等价于 .pfc 控制命令)
|
|
2011
|
+
# ------------------------------------------------------------------
|
|
2012
|
+
def set_limits(self, qres=None, pha=None, del_ang=None,
|
|
2013
|
+
del_volt=None):
|
|
2014
|
+
"""设置潮流限值(等价于 / LIMITS 命令)。
|
|
2015
|
+
|
|
2016
|
+
Parameters
|
|
2017
|
+
----------
|
|
2018
|
+
qres : float, optional
|
|
2019
|
+
Q 残差限值 (MVAr pu)
|
|
2020
|
+
pha : float, optional
|
|
2021
|
+
相角差限值 (度)
|
|
2022
|
+
del_ang : float, optional
|
|
2023
|
+
区域交换功率偏差限值 (MW)
|
|
2024
|
+
del_volt : float, optional
|
|
2025
|
+
电压偏差限值 (%)
|
|
2026
|
+
|
|
2027
|
+
Returns
|
|
2028
|
+
-------
|
|
2029
|
+
int
|
|
2030
|
+
引擎返回码
|
|
2031
|
+
"""
|
|
2032
|
+
cmd = "/ LIMITS"
|
|
2033
|
+
parts = []
|
|
2034
|
+
if qres is not None: parts.append(f"QRES={qres}")
|
|
2035
|
+
if pha is not None: parts.append(f"PHA={pha}")
|
|
2036
|
+
if del_ang is not None: parts.append(f"DEL_ANG={del_ang}")
|
|
2037
|
+
if del_volt is not None: parts.append(f"DEL_VOLT={del_volt}")
|
|
2038
|
+
if parts:
|
|
2039
|
+
cmd += ", " + ", ".join(parts)
|
|
2040
|
+
rc = self.command(cmd)
|
|
2041
|
+
if rc == 0:
|
|
2042
|
+
self._solution_config.setdefault("limits", {}).update(
|
|
2043
|
+
{k: v for k, v in {
|
|
2044
|
+
"qres": qres, "pha": pha, "del_ang": del_ang,
|
|
2045
|
+
"del_volt": del_volt}.items() if v is not None})
|
|
2046
|
+
return rc
|
|
2047
|
+
|
|
2048
|
+
def set_solution_iterations(self, decoupled=None, newton=None,
|
|
2049
|
+
optim=None):
|
|
2050
|
+
"""设置最大迭代次数(等价于 / SOL_ITER 命令)。
|
|
2051
|
+
|
|
2052
|
+
Parameters
|
|
2053
|
+
----------
|
|
2054
|
+
decoupled : int, optional
|
|
2055
|
+
最大分解法迭代次数
|
|
2056
|
+
newton : int, optional
|
|
2057
|
+
最大牛顿法迭代次数
|
|
2058
|
+
optim : int, optional
|
|
2059
|
+
最优乘子法迭代次数
|
|
2060
|
+
|
|
2061
|
+
Returns
|
|
2062
|
+
-------
|
|
2063
|
+
int
|
|
2064
|
+
引擎返回码
|
|
2065
|
+
"""
|
|
2066
|
+
cmd = "/ SOL_ITER"
|
|
2067
|
+
parts = []
|
|
2068
|
+
if decoupled is not None: parts.append(f"DECOUPLED={decoupled}")
|
|
2069
|
+
if newton is not None: parts.append(f"NEWTON={newton}")
|
|
2070
|
+
if optim is not None: parts.append(f"OPTIM={optim}")
|
|
2071
|
+
if parts:
|
|
2072
|
+
cmd += ", " + ", ".join(parts)
|
|
2073
|
+
rc = self.command(cmd)
|
|
2074
|
+
if rc == 0:
|
|
2075
|
+
for key, value in (("decoupled", decoupled),
|
|
2076
|
+
("newton", newton), ("optim", optim)):
|
|
2077
|
+
if value is not None:
|
|
2078
|
+
self._solution_config[key] = int(value)
|
|
2079
|
+
return rc
|
|
2080
|
+
|
|
2081
|
+
def set_misc_controls(self, vflatstart=None, relaxfactor=None,
|
|
2082
|
+
xrghostnode=None, voltagerelax=None,
|
|
2083
|
+
savebe=None):
|
|
2084
|
+
"""设置杂项控制(等价于 / MISC_CNTRL 命令)。
|
|
2085
|
+
|
|
2086
|
+
Parameters
|
|
2087
|
+
----------
|
|
2088
|
+
vflatstart : bool, optional
|
|
2089
|
+
是否启用平启动
|
|
2090
|
+
relaxfactor : float, optional
|
|
2091
|
+
松弛因子
|
|
2092
|
+
xrghostnode : bool, optional
|
|
2093
|
+
是否启用 XRGHOST 节点
|
|
2094
|
+
voltagerelax : bool, optional
|
|
2095
|
+
是否启用电压松弛
|
|
2096
|
+
savebe : bool, optional
|
|
2097
|
+
是否保存 BE 文件
|
|
2098
|
+
|
|
2099
|
+
Returns
|
|
2100
|
+
-------
|
|
2101
|
+
int
|
|
2102
|
+
引擎返回码
|
|
2103
|
+
"""
|
|
2104
|
+
cmd = "/ MISC_CNTRL"
|
|
2105
|
+
parts = []
|
|
2106
|
+
onoff = lambda v: "ON" if v else "OFF"
|
|
2107
|
+
if vflatstart is not None: parts.append(f"VFLATSTART={onoff(vflatstart)}")
|
|
2108
|
+
if relaxfactor is not None: parts.append(f"RELAXfactor={relaxfactor}")
|
|
2109
|
+
if xrghostnode is not None: parts.append(f"XRGHOSTnode={onoff(xrghostnode)}")
|
|
2110
|
+
if voltagerelax is not None: parts.append(f"VOLTAGERelax={onoff(voltagerelax)}")
|
|
2111
|
+
if savebe is not None: parts.append(f"SAVEBE={onoff(savebe)}")
|
|
2112
|
+
if parts:
|
|
2113
|
+
cmd += ", " + ", ".join(parts)
|
|
2114
|
+
rc = self.command(cmd)
|
|
2115
|
+
if rc == 0:
|
|
2116
|
+
if vflatstart is not None:
|
|
2117
|
+
self._solution_config["flat_start"] = bool(vflatstart)
|
|
2118
|
+
self._solution_config.setdefault("misc", {}).update(
|
|
2119
|
+
{k: v for k, v in {
|
|
2120
|
+
"relaxfactor": relaxfactor,
|
|
2121
|
+
"xrghostnode": xrghostnode,
|
|
2122
|
+
"voltagerelax": voltagerelax,
|
|
2123
|
+
"savebe": savebe}.items() if v is not None})
|
|
2124
|
+
return rc
|
|
2125
|
+
|
|
2126
|
+
def set_tolerances(self, bus=None, area=None, tx=None, q=None,
|
|
2127
|
+
opcut=None):
|
|
2128
|
+
"""设置收敛容差(等价于 / TOLERANCE 命令)。
|
|
2129
|
+
|
|
2130
|
+
Parameters
|
|
2131
|
+
----------
|
|
2132
|
+
bus : float, optional
|
|
2133
|
+
母线电压容差 (pu)
|
|
2134
|
+
area : float, optional
|
|
2135
|
+
区域交换功率容差 (MW)
|
|
2136
|
+
tx : float, optional
|
|
2137
|
+
变压器容差 (pu)
|
|
2138
|
+
q : float, optional
|
|
2139
|
+
无功容差 (MVAr)
|
|
2140
|
+
opcut : float, optional
|
|
2141
|
+
最优乘子截止值
|
|
2142
|
+
|
|
2143
|
+
Returns
|
|
2144
|
+
-------
|
|
2145
|
+
int
|
|
2146
|
+
引擎返回码
|
|
2147
|
+
"""
|
|
2148
|
+
cmd = "/ TOLERANCE"
|
|
2149
|
+
parts = []
|
|
2150
|
+
if bus is not None: parts.append(f"BUS={bus}")
|
|
2151
|
+
if area is not None: parts.append(f"AREA={area}")
|
|
2152
|
+
if tx is not None: parts.append(f"TX={tx}")
|
|
2153
|
+
if q is not None: parts.append(f"Q={q}")
|
|
2154
|
+
if opcut is not None: parts.append(f"OPCUT={opcut}")
|
|
2155
|
+
if parts:
|
|
2156
|
+
cmd += ", " + ", ".join(parts)
|
|
2157
|
+
rc = self.command(cmd)
|
|
2158
|
+
if rc == 0:
|
|
2159
|
+
self._solution_config.setdefault("tolerances", {}).update(
|
|
2160
|
+
{k: v for k, v in {
|
|
2161
|
+
"bus": bus, "area": area, "tx": tx,
|
|
2162
|
+
"q": q, "opcut": opcut}.items() if v is not None})
|
|
2163
|
+
return rc
|
|
2164
|
+
|
|
2165
|
+
def set_ltc_control(self, mode="ON"):
|
|
2166
|
+
"""设置 LTC 控制模式(等价于 / LTC 命令)。
|
|
2167
|
+
|
|
2168
|
+
Parameters
|
|
2169
|
+
----------
|
|
2170
|
+
mode : str
|
|
2171
|
+
控制模式: ON, OFF, ON_NV, ON_NPS
|
|
2172
|
+
|
|
2173
|
+
Returns
|
|
2174
|
+
-------
|
|
2175
|
+
int
|
|
2176
|
+
引擎返回码
|
|
2177
|
+
"""
|
|
2178
|
+
return self.command(f"/ LTC = {mode}")
|
|
2179
|
+
|
|
2180
|
+
def set_input_listing(self, mode="FULL"):
|
|
2181
|
+
"""设置输入列表(等价于 ./ P_INPUTLIST 命令)。
|
|
2182
|
+
|
|
2183
|
+
Parameters
|
|
2184
|
+
----------
|
|
2185
|
+
mode : str
|
|
2186
|
+
列表模式: FULL, NONE, ZONES=<z1,z2,...>
|
|
2187
|
+
|
|
2188
|
+
Returns
|
|
2189
|
+
-------
|
|
2190
|
+
int
|
|
2191
|
+
引擎返回码
|
|
2192
|
+
"""
|
|
2193
|
+
return self.command(f"./ P_INPUTLIST,{mode}")
|
|
2194
|
+
|
|
2195
|
+
def set_output_listing(self, mode="FULL"):
|
|
2196
|
+
"""设置输出列表(等价于 ./ P_OUTPUTLIST 命令)。
|
|
2197
|
+
|
|
2198
|
+
Parameters
|
|
2199
|
+
----------
|
|
2200
|
+
mode : str
|
|
2201
|
+
列表模式: FULL, NONE, ZONES=<z1,z2,...>
|
|
2202
|
+
|
|
2203
|
+
Returns
|
|
2204
|
+
-------
|
|
2205
|
+
int
|
|
2206
|
+
引擎返回码
|
|
2207
|
+
"""
|
|
2208
|
+
return self.command(f"./ P_OUTPUTLIST,{mode}")
|
|
2209
|
+
|
|
2210
|
+
def set_analysis_report(self, level=1, zones=None, owners=None):
|
|
2211
|
+
"""设置分析报表级别(等价于 / P_ANALYSIS_RPT 命令)。
|
|
2212
|
+
|
|
2213
|
+
Parameters
|
|
2214
|
+
----------
|
|
2215
|
+
level : int
|
|
2216
|
+
报表级别: 1-4(1=仅 UNSCH, 2=1-17 号报表,
|
|
2217
|
+
3=1-18 号, 4=全部 23 号)
|
|
2218
|
+
zones : str, optional
|
|
2219
|
+
分区过滤
|
|
2220
|
+
owners : str, optional
|
|
2221
|
+
所有者过滤
|
|
2222
|
+
|
|
2223
|
+
Returns
|
|
2224
|
+
-------
|
|
2225
|
+
int
|
|
2226
|
+
引擎返回码
|
|
2227
|
+
"""
|
|
2228
|
+
cmd = f"/ P_ANALYSIS_RPT,LEVEL={level}"
|
|
2229
|
+
if zones:
|
|
2230
|
+
cmd += f", ZONES={zones}"
|
|
2231
|
+
if owners:
|
|
2232
|
+
cmd += f", OWNERS={owners}"
|
|
2233
|
+
return self.command(cmd)
|
|
2234
|
+
|
|
2235
|
+
def set_mva_base(self, mva):
|
|
2236
|
+
"""设置系统 MVA 基准值(等价于 / MVA_BASE 命令)。
|
|
2237
|
+
|
|
2238
|
+
Parameters
|
|
2239
|
+
----------
|
|
2240
|
+
mva : float
|
|
2241
|
+
MVA 基准值
|
|
2242
|
+
|
|
2243
|
+
Returns
|
|
2244
|
+
-------
|
|
2245
|
+
int
|
|
2246
|
+
引擎返回码
|
|
2247
|
+
"""
|
|
2248
|
+
return self.command(f"/ MVA_BASE = {mva}")
|
|
2249
|
+
|
|
2250
|
+
def select_base(self, base):
|
|
2251
|
+
value = _ascii(base)
|
|
2252
|
+
if not isinstance(value, bytes) or len(value) != 1:
|
|
2253
|
+
raise ValueError("base 必须是单字符 'O' 或 'R'")
|
|
2254
|
+
self._invalidate_cache()
|
|
2255
|
+
return self._lib.pf_select_base(value)
|
|
2256
|
+
|
|
2257
|
+
def solve_area(self, base):
|
|
2258
|
+
value = _ascii(base)
|
|
2259
|
+
if not isinstance(value, bytes) or len(value) != 1:
|
|
2260
|
+
raise ValueError("base 必须是单字符 'O' 或 'R'")
|
|
2261
|
+
return self._lib.pf_solve_area(value)
|
|
2262
|
+
|
|
2263
|
+
def bus_exists(self, name, kv):
|
|
2264
|
+
return self._lib.pf_bus_exists(_ascii(name), c_float(kv))
|
|
2265
|
+
|
|
2266
|
+
def area_of_zone(self, area, zone):
|
|
2267
|
+
return self._lib.pf_area_of_zone(_ascii(area), _ascii(zone))
|
|
2268
|
+
|
|
2269
|
+
# ------------------------------------------------------------------
|
|
2270
|
+
# 修改类(/changes 记录)
|
|
2271
|
+
# ------------------------------------------------------------------
|
|
2272
|
+
def rename_bus(self, oldname, oldkv, newname, newkv):
|
|
2273
|
+
"""母线改名(ZB 变更记录)。"""
|
|
2274
|
+
rc = self._lib.pf_rename_bus(_ascii(oldname), c_float(oldkv),
|
|
2275
|
+
_ascii(newname), c_float(newkv))
|
|
2276
|
+
self._invalidate_cache()
|
|
2277
|
+
return rc
|
|
2278
|
+
|
|
2279
|
+
def rename_area(self, oldname, newname):
|
|
2280
|
+
"""控制区改名(ZA 变更记录)。"""
|
|
2281
|
+
rc = self._lib.pf_rename_area(_ascii(oldname), _ascii(newname))
|
|
2282
|
+
self._invalidate_cache()
|
|
2283
|
+
return rc
|
|
2284
|
+
|
|
2285
|
+
def rename_zone(self, oldname, newname):
|
|
2286
|
+
"""分区改名(Z 变更记录)。"""
|
|
2287
|
+
rc = self._lib.pf_rename_zone(_ascii(oldname), _ascii(newname))
|
|
2288
|
+
self._invalidate_cache()
|
|
2289
|
+
return rc
|
|
2290
|
+
|
|
2291
|
+
def del_area(self, area):
|
|
2292
|
+
"""按控制区删除(DA 变更记录)。
|
|
2293
|
+
|
|
2294
|
+
警告:该卡删除属于该控制区的**全部母线**(chread.f "DA" 语义),
|
|
2295
|
+
而非仅删除区域定义;不存在的区域名返回非零状态。
|
|
2296
|
+
"""
|
|
2297
|
+
rc = self._lib.pf_del_area(_ascii(area))
|
|
2298
|
+
self._invalidate_cache()
|
|
2299
|
+
return rc
|
|
2300
|
+
|
|
2301
|
+
def del_zone(self, zone):
|
|
2302
|
+
"""按分区删除(DZ 变更记录)。
|
|
2303
|
+
|
|
2304
|
+
警告:该卡删除属于该分区的**全部母线**(chread.f "DZ" 语义),
|
|
2305
|
+
而非仅删除分区定义;不存在的分区名静默成功(0 条母线被删)。
|
|
2306
|
+
"""
|
|
2307
|
+
rc = self._lib.pf_del_zone(_ascii(zone))
|
|
2308
|
+
self._invalidate_cache()
|
|
2309
|
+
return rc
|
|
2310
|
+
|
|
2311
|
+
def save_netdata(self, filename, dialect="BPA", ratings="EXTENDED",
|
|
2312
|
+
size=120):
|
|
2313
|
+
"""导出网络数据(/save_file, type=network_data)。
|
|
2314
|
+
|
|
2315
|
+
dialect: BPA / WSCC / WSCC1 / PTI;
|
|
2316
|
+
ratings: EXTENDED / MINIMUM / NOMINAL;
|
|
2317
|
+
size: 每行字符数 120 或 80。
|
|
2318
|
+
"""
|
|
2319
|
+
return self._lib.pf_save_netdata(_ascii(filename), _ascii(dialect),
|
|
2320
|
+
_ascii(ratings), c_int(size))
|
|
2321
|
+
|
|
2322
|
+
def save_wscc_stab_data(self, filename, type="ascii_stability"):
|
|
2323
|
+
"""导出 WSCC 稳定数据;type: ascii_stability / binary_stability。"""
|
|
2324
|
+
return self._lib.pf_save_wscc_stab_data(_ascii(filename),
|
|
2325
|
+
_ascii(type))
|
|
2326
|
+
|
|
2327
|
+
# ------------------------------------------------------------------
|
|
2328
|
+
# 工具箱直调(原 puf / ipfcut 独立程序,已 API 化;报表走 /REPORTS 命令)
|
|
2329
|
+
# ------------------------------------------------------------------
|
|
2330
|
+
def puf(self, args):
|
|
2331
|
+
"""路径使用因子(PUF)分析,参数与命令行版 puf 相同。
|
|
2332
|
+
|
|
2333
|
+
args: 字符串列表,例如 ["-G", "base.bse", "out.txt", "brn.dat"];
|
|
2334
|
+
位置参数依次为 BASE 文件、输出文件、支路数据文件。
|
|
2335
|
+
返回 0 成功;-1 表示参数个数非法。
|
|
2336
|
+
"""
|
|
2337
|
+
argv_list = [_ascii(a) for a in args]
|
|
2338
|
+
argc = len(argv_list)
|
|
2339
|
+
argv = (c_char_p * argc)(*argv_list) if argc > 0 else None
|
|
2340
|
+
rc = self._lib.bpaflow_puf(c_int(argc), argv)
|
|
2341
|
+
self._invalidate_cache()
|
|
2342
|
+
return rc
|
|
2343
|
+
|
|
2344
|
+
def ipfcut(self, out_file, base_file, cmd_file):
|
|
2345
|
+
"""电网切割:out_file=BUS/BRANCH 输出,base_file=基态,
|
|
2346
|
+
cmd_file=CUT 命令文件。返回 0 成功,1=CUT 文件不存在,
|
|
2347
|
+
2=BUS/BRANCH 打开失败,3=命令文件打开失败,4=基态加载失败。"""
|
|
2348
|
+
rc = self._lib.bpaflow_ipfcut(_ascii(out_file), _ascii(base_file),
|
|
2349
|
+
_ascii(cmd_file))
|
|
2350
|
+
self._invalidate_cache()
|
|
2351
|
+
return rc
|
|
2352
|
+
|
|
2353
|
+
def reports(self, out_file, selects):
|
|
2354
|
+
"""潮流报表:经 /REPORTS 命令生成报表文件。
|
|
2355
|
+
|
|
2356
|
+
selects: 报表名或列表,如 "BUS_INPUT"、["BUS_INPUT", "LINEFLOW"]。
|
|
2357
|
+
语法要点:引擎要求 `select NAME` 空格分隔且不带 '=',带 '=' 会
|
|
2358
|
+
落入通用回退分支;每条命令只处理第一个 select;引擎的 append
|
|
2359
|
+
模式在 gfortran 下存在缺陷(open_apnd),因此多选时本方法自动
|
|
2360
|
+
分文件生成再合并。返回引擎 rc(0 成功)。
|
|
2361
|
+
"""
|
|
2362
|
+
if isinstance(selects, str):
|
|
2363
|
+
selects = [selects]
|
|
2364
|
+
targets = [out_file] if len(selects) == 1 else [
|
|
2365
|
+
"%s.part%d" % (out_file, i) for i in range(len(selects))]
|
|
2366
|
+
rc = 0
|
|
2367
|
+
for tgt, sel in zip(targets, selects):
|
|
2368
|
+
rc = self.command("/reports, overwrite = %s, select %s"
|
|
2369
|
+
% (tgt, sel))
|
|
2370
|
+
if rc != 0:
|
|
2371
|
+
break
|
|
2372
|
+
if rc == 0 and len(targets) > 1:
|
|
2373
|
+
with open(out_file, "wb") as out:
|
|
2374
|
+
for tgt in targets:
|
|
2375
|
+
with open(tgt, "rb") as f:
|
|
2376
|
+
out.write(f.read())
|
|
2377
|
+
for tgt in targets:
|
|
2378
|
+
try:
|
|
2379
|
+
os.remove(tgt)
|
|
2380
|
+
except OSError:
|
|
2381
|
+
pass
|
|
2382
|
+
return rc
|
|
2383
|
+
|
|
2384
|
+
def get_list(self, list_type, maxlen=4096):
|
|
2385
|
+
"""按类型取列表(AREA/BUS/KV/OWNER/REC_TYPE/ZONE)。
|
|
2386
|
+
|
|
2387
|
+
pf_get_list 的 listlen 参数是"条目数"(每条定长),故缓冲区按
|
|
2388
|
+
maxlen * 最大条目宽 16 字节分配,避免 C 侧越界写。
|
|
2389
|
+
"""
|
|
2390
|
+
list_type = int(list_type)
|
|
2391
|
+
maxlen = int(maxlen)
|
|
2392
|
+
if list_type not in range(6):
|
|
2393
|
+
raise ValueError("list_type 必须是 BPAFlow.*_LIST(0..5)")
|
|
2394
|
+
if maxlen <= 0:
|
|
2395
|
+
raise ValueError("maxlen 必须为正整数")
|
|
2396
|
+
buf = create_string_buffer(maxlen * 16)
|
|
2397
|
+
rc = self._lib.pf_get_list(buf, maxlen, list_type, b"")
|
|
2398
|
+
if rc != 0:
|
|
2399
|
+
raise PyFlowError("pf_get_list 失败, rc=%d" % rc)
|
|
2400
|
+
parts = bytes(buf.raw).split(b"\x00")
|
|
2401
|
+
return [p.decode("ascii", "replace") for p in parts if p]
|
|
2402
|
+
|
|
2403
|
+
# ------------------------------------------------------------------
|
|
2404
|
+
# 算例概况
|
|
2405
|
+
# ------------------------------------------------------------------
|
|
2406
|
+
def case_info(self):
|
|
2407
|
+
"""返回算例概况字典(含完整 ``pf_case_stats`` 与求解状态)。
|
|
2408
|
+
|
|
2409
|
+
``num_branches`` 是引擎的双向支路记录数;``num_circuits`` 是
|
|
2410
|
+
物理回路统计口径(并行线路组按一个 circuit 计数)。
|
|
2411
|
+
"""
|
|
2412
|
+
base_mva = c_float()
|
|
2413
|
+
stats = (c_int * 20)()
|
|
2414
|
+
ver = create_string_buffer(11)
|
|
2415
|
+
self._lib.ipfpy_case_info_ext(byref(base_mva), stats, ver)
|
|
2416
|
+
return {
|
|
2417
|
+
"pf_version": _text(ver.raw),
|
|
2418
|
+
"base_mva": base_mva.value,
|
|
2419
|
+
"num_DC_systems": stats[0],
|
|
2420
|
+
"num_areas": stats[1],
|
|
2421
|
+
"num_ities": stats[2],
|
|
2422
|
+
"num_interties": stats[2],
|
|
2423
|
+
"num_zones": stats[3],
|
|
2424
|
+
"num_owners": stats[4],
|
|
2425
|
+
"num_buses": stats[5],
|
|
2426
|
+
"num_area_slack_buses": stats[6],
|
|
2427
|
+
"num_DC_buses": stats[7],
|
|
2428
|
+
"num_AGC_buses": stats[8],
|
|
2429
|
+
"num_BX_buses": stats[9],
|
|
2430
|
+
"num_adjustable_buses": stats[10],
|
|
2431
|
+
"num_pct_var_ctrl_buses": stats[11],
|
|
2432
|
+
"num_branches": stats[12],
|
|
2433
|
+
"num_circuits": stats[13],
|
|
2434
|
+
"num_DC_lines": stats[14],
|
|
2435
|
+
"num_LTC_xfmrs": stats[15],
|
|
2436
|
+
"num_phase_shifters": stats[16],
|
|
2437
|
+
"soln_status": stats[17],
|
|
2438
|
+
"num_diff_kv": stats[18],
|
|
2439
|
+
"num_rec_types": stats[19],
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
# ------------------------------------------------------------------
|
|
2443
|
+
# 快照/回滚(P1,基于临时 .bse 文件)
|
|
2444
|
+
# ------------------------------------------------------------------
|
|
2445
|
+
def snapshot(self, tag=None):
|
|
2446
|
+
"""保存当前算例快照到临时文件。
|
|
2447
|
+
|
|
2448
|
+
tag 可选标识,用于 restore(tag=tag)。返回标识串(缺省为文件
|
|
2449
|
+
路径)。快照文件在进程退出时由 tempdir 清理,也可显式调用
|
|
2450
|
+
drop_snapshot(tag=tag)。
|
|
2451
|
+
|
|
2452
|
+
用法::
|
|
2453
|
+
tag = pf.snapshot("before_N1")
|
|
2454
|
+
# ... apply changes, solve ...
|
|
2455
|
+
pf.restore(tag)
|
|
2456
|
+
"""
|
|
2457
|
+
import tempfile
|
|
2458
|
+
fd, path = tempfile.mkstemp(prefix="bpaflow_snap_", suffix=".bse")
|
|
2459
|
+
os.close(fd)
|
|
2460
|
+
rc = self.save_newbase(path)
|
|
2461
|
+
if rc != 0:
|
|
2462
|
+
# 引擎 /save_file 偶尔瞬态失败(如 PCL 命令排队中);
|
|
2463
|
+
# 重试一次
|
|
2464
|
+
rc = self.save_newbase(path)
|
|
2465
|
+
if rc != 0:
|
|
2466
|
+
os.unlink(path)
|
|
2467
|
+
raise PyFlowError("快照保存失败 rc=%d(path=%r)" % (rc, path))
|
|
2468
|
+
tag = tag if tag is not None else path
|
|
2469
|
+
old_path = self._snapshots.get(tag)
|
|
2470
|
+
if old_path and old_path != path:
|
|
2471
|
+
try:
|
|
2472
|
+
os.unlink(old_path)
|
|
2473
|
+
except OSError:
|
|
2474
|
+
pass
|
|
2475
|
+
self._snapshots[tag] = path
|
|
2476
|
+
return tag
|
|
2477
|
+
|
|
2478
|
+
def restore(self, tag=None):
|
|
2479
|
+
"""恢复到快照状态(加载 .bse 临时文件,默认最近一个快照)。"""
|
|
2480
|
+
if tag is None:
|
|
2481
|
+
if not self._snapshots:
|
|
2482
|
+
raise PyFlowError("无可用快照")
|
|
2483
|
+
tag = list(self._snapshots.keys())[-1]
|
|
2484
|
+
path = self._snapshots.get(tag)
|
|
2485
|
+
if path is None:
|
|
2486
|
+
raise PyFlowError("快照 %r 不存在" % tag)
|
|
2487
|
+
rc = self.load_oldbase(path)
|
|
2488
|
+
if rc != 0:
|
|
2489
|
+
raise PyFlowError("快照恢复失败 rc=%d" % rc)
|
|
2490
|
+
return rc
|
|
2491
|
+
|
|
2492
|
+
def drop_snapshot(self, tag=None):
|
|
2493
|
+
"""删除快照临时文件。不传 tag 则删除全部。"""
|
|
2494
|
+
if tag is None:
|
|
2495
|
+
for p in self._snapshots.values():
|
|
2496
|
+
try:
|
|
2497
|
+
os.unlink(p)
|
|
2498
|
+
except OSError:
|
|
2499
|
+
pass
|
|
2500
|
+
self._snapshots.clear()
|
|
2501
|
+
else:
|
|
2502
|
+
path = self._snapshots.pop(tag, None)
|
|
2503
|
+
if path:
|
|
2504
|
+
try:
|
|
2505
|
+
os.unlink(path)
|
|
2506
|
+
except OSError:
|
|
2507
|
+
pass
|
|
2508
|
+
|
|
2509
|
+
# ------------------------------------------------------------------
|
|
2510
|
+
# 母线/支路/控制区(扁平数组导出,免 ctypes 复刻 pf_rec)
|
|
2511
|
+
# ------------------------------------------------------------------
|
|
2512
|
+
def _buses_all(self):
|
|
2513
|
+
"""无过滤的母线全量查询,带缓存与 refbase 扩容。"""
|
|
2514
|
+
if "buses_all" in self._cache:
|
|
2515
|
+
return self._cache["buses_all"]
|
|
2516
|
+
ci = self.case_info()
|
|
2517
|
+
n = int(ci.get("num_buses") or 0)
|
|
2518
|
+
maxbus = max(n * 2, 64)
|
|
2519
|
+
while True:
|
|
2520
|
+
fields = (c_double * (maxbus * 15))()
|
|
2521
|
+
names_arr = ((c_char * 9) * maxbus)()
|
|
2522
|
+
types_arr = ((c_char * 3) * maxbus)()
|
|
2523
|
+
zones_arr = ((c_char * 3) * maxbus)()
|
|
2524
|
+
kvs = (c_float * maxbus)()
|
|
2525
|
+
got = self._lib.ipfpy_get_buses(
|
|
2526
|
+
ctypes.byref(fields), ctypes.byref(names_arr),
|
|
2527
|
+
ctypes.byref(types_arr), ctypes.byref(zones_arr),
|
|
2528
|
+
ctypes.byref(kvs), maxbus)
|
|
2529
|
+
if got >= 0:
|
|
2530
|
+
break
|
|
2531
|
+
if maxbus >= MAXBUS * 2:
|
|
2532
|
+
raise PyFlowError("ipfpy_get_buses 缓冲区不足")
|
|
2533
|
+
maxbus = min(maxbus * 2, MAXBUS * 2)
|
|
2534
|
+
out = []
|
|
2535
|
+
f = fields
|
|
2536
|
+
for i in range(got):
|
|
2537
|
+
btype = _text(types_arr[i].raw)
|
|
2538
|
+
out.append({
|
|
2539
|
+
"name": _text(names_arr[i].raw),
|
|
2540
|
+
"type": btype,
|
|
2541
|
+
"bus_kind": "dc" if btype.strip().upper() in ("BD", "BM") else "ac",
|
|
2542
|
+
"is_dc": btype.strip().upper() in ("BD", "BM"),
|
|
2543
|
+
"zone": _text(zones_arr[i].raw),
|
|
2544
|
+
"kv": kvs[i],
|
|
2545
|
+
"Pgen": f[i * 15 + 0], "Qgen": f[i * 15 + 1],
|
|
2546
|
+
"Vmag": f[i * 15 + 2], "Vdeg": f[i * 15 + 3],
|
|
2547
|
+
"Pload": f[i * 15 + 4], "Qload": f[i * 15 + 5],
|
|
2548
|
+
"Pshunt": f[i * 15 + 6], "Qshunt": f[i * 15 + 7],
|
|
2549
|
+
"Qsch_Qmax": f[i * 15 + 8], "Qmin": f[i * 15 + 9],
|
|
2550
|
+
"Vhold_Vmax": f[i * 15 + 10], "Vmin_Vdeg": f[i * 15 + 11],
|
|
2551
|
+
"pct_vars": f[i * 15 + 12], "Qunsch": f[i * 15 + 13],
|
|
2552
|
+
"Bshunt_used": f[i * 15 + 14],
|
|
2553
|
+
})
|
|
2554
|
+
self._cache["buses_all"] = out
|
|
2555
|
+
return out
|
|
2556
|
+
|
|
2557
|
+
def buses(self, zone=None, kv=None, name=None, bus_names=None,
|
|
2558
|
+
filter_zone=None):
|
|
2559
|
+
"""返回母线列表,每项含输入与求解字段(字典)。
|
|
2560
|
+
|
|
2561
|
+
字段包含 `bus_kind`/`is_dc`;DC 母线(BD/BM)不参与 AC 电压、
|
|
2562
|
+
q_limited 和孤岛扫描。
|
|
2563
|
+
|
|
2564
|
+
可选过滤参数(Python 侧过滤):
|
|
2565
|
+
zone 分区码(2 字符,如 "30")
|
|
2566
|
+
kv 基准电压 kV
|
|
2567
|
+
name 母线名
|
|
2568
|
+
bus_names 母线名集合(set/list),匹配其一即返回
|
|
2569
|
+
filter_zone 同 zone(别名)
|
|
2570
|
+
"""
|
|
2571
|
+
zone = zone or filter_zone
|
|
2572
|
+
out = self._buses_all()
|
|
2573
|
+
if zone:
|
|
2574
|
+
out = [b for b in out if b["zone"].strip() == zone]
|
|
2575
|
+
if kv is not None:
|
|
2576
|
+
out = [b for b in out if abs(b["kv"] - kv) < 0.005]
|
|
2577
|
+
if name is not None:
|
|
2578
|
+
nm = name.strip().upper()
|
|
2579
|
+
out = [b for b in out if b["name"].strip().upper() == nm]
|
|
2580
|
+
if bus_names is not None:
|
|
2581
|
+
nms = set(n.strip().upper() for n in bus_names)
|
|
2582
|
+
out = [b for b in out if b["name"].strip().upper() in nms]
|
|
2583
|
+
return [dict(b) for b in out]
|
|
2584
|
+
|
|
2585
|
+
def bus(self, name, kv):
|
|
2586
|
+
"""返回单条母线(字典)或 None。
|
|
2587
|
+
|
|
2588
|
+
用法::
|
|
2589
|
+
b = pf.bus("BUS1", 230.0)
|
|
2590
|
+
if b is not None:
|
|
2591
|
+
print(b["Vmag"], b["Pgen"])
|
|
2592
|
+
"""
|
|
2593
|
+
for b in self.buses():
|
|
2594
|
+
if (b["name"].strip().upper() == name.strip().upper()
|
|
2595
|
+
and abs(b["kv"] - kv) < 0.005):
|
|
2596
|
+
return b
|
|
2597
|
+
return None
|
|
2598
|
+
|
|
2599
|
+
def _branches_all(self, input_only=False):
|
|
2600
|
+
"""无过滤支路全量查询,带缓存和 refbase 扩容。"""
|
|
2601
|
+
cache_key = "branches_input" if input_only else "branches_solved"
|
|
2602
|
+
if cache_key in self._cache:
|
|
2603
|
+
return self._cache[cache_key]
|
|
2604
|
+
ci = self.case_info()
|
|
2605
|
+
n = int(ci.get("num_branches") or 0)
|
|
2606
|
+
maxbr = max(n * 2 + 64, 64)
|
|
2607
|
+
use_ext = hasattr(self._lib, "ipfpy_get_branches_ext")
|
|
2608
|
+
while True:
|
|
2609
|
+
if use_ext:
|
|
2610
|
+
fields = (c_double * (maxbr * 29))()
|
|
2611
|
+
names1_arr = ((c_char * 9) * maxbr)()
|
|
2612
|
+
names2_arr = ((c_char * 9) * maxbr)()
|
|
2613
|
+
types_arr = ((c_char * 3) * maxbr)()
|
|
2614
|
+
ckts_arr = (c_char * maxbr)()
|
|
2615
|
+
owners_arr = ((c_char * 4) * maxbr)()
|
|
2616
|
+
kvs1 = (c_float * maxbr)()
|
|
2617
|
+
kvs2 = (c_float * maxbr)()
|
|
2618
|
+
sections_arr = (c_int * maxbr)()
|
|
2619
|
+
codes_arr = (c_char * (maxbr * 2))()
|
|
2620
|
+
got = self._lib.ipfpy_get_branches_ext(
|
|
2621
|
+
ctypes.byref(fields), ctypes.byref(names1_arr),
|
|
2622
|
+
ctypes.byref(names2_arr), ctypes.byref(types_arr),
|
|
2623
|
+
ctypes.byref(ckts_arr), ctypes.byref(owners_arr),
|
|
2624
|
+
ctypes.byref(kvs1), ctypes.byref(kvs2),
|
|
2625
|
+
ctypes.byref(sections_arr), ctypes.byref(codes_arr),
|
|
2626
|
+
maxbr, 0 if input_only else 1)
|
|
2627
|
+
else:
|
|
2628
|
+
fields = (c_double * (maxbr * 15))()
|
|
2629
|
+
names1_arr = ((c_char * 9) * maxbr)()
|
|
2630
|
+
names2_arr = ((c_char * 9) * maxbr)()
|
|
2631
|
+
types_arr = ((c_char * 3) * maxbr)()
|
|
2632
|
+
ckts_arr = (c_char * maxbr)()
|
|
2633
|
+
kvs1 = (c_float * maxbr)()
|
|
2634
|
+
kvs2 = (c_float * maxbr)()
|
|
2635
|
+
getter = (self._lib.ipfpy_get_branches_input if input_only
|
|
2636
|
+
else self._lib.ipfpy_get_branches)
|
|
2637
|
+
got = getter(
|
|
2638
|
+
ctypes.byref(fields), ctypes.byref(names1_arr),
|
|
2639
|
+
ctypes.byref(names2_arr), ctypes.byref(types_arr),
|
|
2640
|
+
ctypes.byref(ckts_arr), ctypes.byref(kvs1),
|
|
2641
|
+
ctypes.byref(kvs2), maxbr)
|
|
2642
|
+
if got >= 0:
|
|
2643
|
+
break
|
|
2644
|
+
if maxbr >= MAXBUS * 8:
|
|
2645
|
+
raise PyFlowError("ipfpy_get_branches 缓冲区不足")
|
|
2646
|
+
maxbr = min(maxbr * 2, MAXBUS * 8)
|
|
2647
|
+
|
|
2648
|
+
out = []
|
|
2649
|
+
for i in range(got):
|
|
2650
|
+
if use_ext:
|
|
2651
|
+
f = fields
|
|
2652
|
+
base = i * 29
|
|
2653
|
+
br = {
|
|
2654
|
+
"type": _text(types_arr[i].raw),
|
|
2655
|
+
"bus1_name": _text(names1_arr[i].raw),
|
|
2656
|
+
"bus2_name": _text(names2_arr[i].raw),
|
|
2657
|
+
"kv1": kvs1[i], "kv2": kvs2[i],
|
|
2658
|
+
"ckt": ckts_arr.raw[i:i + 1].decode("ascii", "replace"),
|
|
2659
|
+
"owner": _text(owners_arr[i].raw),
|
|
2660
|
+
"section": int(sections_arr[i]),
|
|
2661
|
+
"r": f[base + 0], "x": f[base + 1],
|
|
2662
|
+
"g": f[base + 2], "b": f[base + 3],
|
|
2663
|
+
"tap1": f[base + 4], "tap2": f[base + 5],
|
|
2664
|
+
"total_rating": f[base + 6],
|
|
2665
|
+
"num_ckts": int(f[base + 7]),
|
|
2666
|
+
"Pin": f[base + 8], "Qin": f[base + 9],
|
|
2667
|
+
"Pout": f[base + 10], "Qout": f[base + 11],
|
|
2668
|
+
"Ploss": f[base + 12], "Qloss": f[base + 13],
|
|
2669
|
+
"meter": int(f[base + 14]),
|
|
2670
|
+
"thermal_rating": f[base + 15],
|
|
2671
|
+
"bottleneck_rating": f[base + 16],
|
|
2672
|
+
"emergency_rating": f[base + 17],
|
|
2673
|
+
"crit_line_load_amps": f[base + 18],
|
|
2674
|
+
"crit_line_rat_amps": f[base + 19],
|
|
2675
|
+
"tot_line_load_pct": f[base + 20],
|
|
2676
|
+
"crit_line_load_term": int(f[base + 21]),
|
|
2677
|
+
"crit_xfmr_load_mva": f[base + 22],
|
|
2678
|
+
"crit_xfmr_rat_mva": f[base + 23],
|
|
2679
|
+
"tot_xfmr_load_pct": f[base + 24],
|
|
2680
|
+
"crit_xfmr_load_term": int(f[base + 25]),
|
|
2681
|
+
"tot_line_load_amps": f[base + 26],
|
|
2682
|
+
"tot_xfmr_load_mva": f[base + 27],
|
|
2683
|
+
"solution_available": bool(f[base + 28]) and not input_only,
|
|
2684
|
+
"crit_line_rat_code": (
|
|
2685
|
+
codes_arr.raw[i * 2:i * 2 + 1].decode(
|
|
2686
|
+
"ascii", "replace").strip() or "N"),
|
|
2687
|
+
"crit_xfmr_rat_code": (
|
|
2688
|
+
codes_arr.raw[i * 2 + 1:i * 2 + 2].decode(
|
|
2689
|
+
"ascii", "replace").strip() or "N"),
|
|
2690
|
+
}
|
|
2691
|
+
else:
|
|
2692
|
+
f = fields
|
|
2693
|
+
base = i * 15
|
|
2694
|
+
br = {
|
|
2695
|
+
"type": _text(types_arr[i].raw),
|
|
2696
|
+
"bus1_name": _text(names1_arr[i].raw),
|
|
2697
|
+
"bus2_name": _text(names2_arr[i].raw),
|
|
2698
|
+
"kv1": kvs1[i], "kv2": kvs2[i],
|
|
2699
|
+
"ckt": ckts_arr.raw[i:i + 1].decode("ascii", "replace"),
|
|
2700
|
+
"owner": "", "section": 0,
|
|
2701
|
+
"r": f[base + 0], "x": f[base + 1],
|
|
2702
|
+
"g": f[base + 2], "b": f[base + 3],
|
|
2703
|
+
"tap1": f[base + 4], "tap2": f[base + 5],
|
|
2704
|
+
"total_rating": f[base + 6],
|
|
2705
|
+
"num_ckts": int(f[base + 7]),
|
|
2706
|
+
"Pin": f[base + 8], "Qin": f[base + 9],
|
|
2707
|
+
"Pout": f[base + 10], "Qout": f[base + 11],
|
|
2708
|
+
"Ploss": f[base + 12], "Qloss": f[base + 13],
|
|
2709
|
+
"meter": int(f[base + 14]),
|
|
2710
|
+
"thermal_rating": 0.0, "bottleneck_rating": 0.0,
|
|
2711
|
+
"emergency_rating": 0.0,
|
|
2712
|
+
"crit_line_load_amps": 0.0,
|
|
2713
|
+
"crit_line_rat_amps": 0.0,
|
|
2714
|
+
"tot_line_load_pct": 0.0, "crit_line_load_term": 0,
|
|
2715
|
+
"crit_xfmr_load_mva": 0.0,
|
|
2716
|
+
"crit_xfmr_rat_mva": 0.0,
|
|
2717
|
+
"tot_xfmr_load_pct": 0.0, "crit_xfmr_load_term": 0,
|
|
2718
|
+
"tot_line_load_amps": 0.0, "tot_xfmr_load_mva": 0.0,
|
|
2719
|
+
"solution_available": not input_only,
|
|
2720
|
+
"crit_line_rat_code": "N",
|
|
2721
|
+
"crit_xfmr_rat_code": "N",
|
|
2722
|
+
}
|
|
2723
|
+
br.update(_branch_loading(br))
|
|
2724
|
+
br["branch_kind"] = _branch_kind(br["type"])
|
|
2725
|
+
if br["branch_kind"] not in ("line", "transformer"):
|
|
2726
|
+
br["solution_available"] = False
|
|
2727
|
+
for key in ("Pin", "Qin", "Pout", "Qout",
|
|
2728
|
+
"Ploss", "Qloss"):
|
|
2729
|
+
br[key] = 0.0
|
|
2730
|
+
out.append(br)
|
|
2731
|
+
self._cache[cache_key] = out
|
|
2732
|
+
return out
|
|
2733
|
+
|
|
2734
|
+
def branches(self, brtype=None, kv=None, bus1=None, bus2=None,
|
|
2735
|
+
input_only=False, include_outaged=False):
|
|
2736
|
+
"""返回支路列表,每项含输入、求解和负载率字段。
|
|
2737
|
+
|
|
2738
|
+
新增 owner/section/thermal_rating/bottleneck_rating/
|
|
2739
|
+
emergency_rating、crit_line_*/crit_xfmr_*/tot_* 负载率字段,
|
|
2740
|
+
以及 loading_ratio/loading_pct/loading_kind/rating_used/
|
|
2741
|
+
rating_code/s_mva/i_amps/in_service/status。
|
|
2742
|
+
|
|
2743
|
+
input_only=True 时不读取求解量。include_outaged=True 时把本实例
|
|
2744
|
+
通过 outage_branch 开断、尚未恢复的支路以 in_service=False
|
|
2745
|
+
追加入结果;基础算例中已经停运、从未出现在内核支路表中的支路
|
|
2746
|
+
无法由该接口还原。
|
|
2747
|
+
"""
|
|
2748
|
+
out = [dict(b) for b in self._branches_all(input_only)]
|
|
2749
|
+
for b in out:
|
|
2750
|
+
b["in_service"] = True
|
|
2751
|
+
b["status"] = "IN"
|
|
2752
|
+
if include_outaged and not input_only:
|
|
2753
|
+
active_keys = set(_branch_dedup_key(b) for b in out)
|
|
2754
|
+
for snap in self._branch_snapshots.values():
|
|
2755
|
+
if _branch_dedup_key(snap) in active_keys:
|
|
2756
|
+
continue
|
|
2757
|
+
b = dict(snap)
|
|
2758
|
+
b.update({"in_service": False, "status": "OUT",
|
|
2759
|
+
"solution_available": False})
|
|
2760
|
+
b.update(_branch_loading(b))
|
|
2761
|
+
out.append(b)
|
|
2762
|
+
if brtype is not None:
|
|
2763
|
+
bt = brtype.strip().upper()
|
|
2764
|
+
out = [b for b in out if b["type"].strip().upper() == bt]
|
|
2765
|
+
if kv is not None:
|
|
2766
|
+
out = [b for b in out if abs(b["kv1"] - kv) < 0.005
|
|
2767
|
+
or abs(b["kv2"] - kv) < 0.005]
|
|
2768
|
+
if bus1 is not None:
|
|
2769
|
+
b1 = bus1.strip().upper()
|
|
2770
|
+
out = [b for b in out if b["bus1_name"].strip().upper() == b1]
|
|
2771
|
+
if bus2 is not None:
|
|
2772
|
+
b2 = bus2.strip().upper()
|
|
2773
|
+
out = [b for b in out if b["bus2_name"].strip().upper() == b2]
|
|
2774
|
+
return out
|
|
2775
|
+
|
|
2776
|
+
def branch(self, bus1, kv1, bus2, kv2, ckt=None, section=None,
|
|
2777
|
+
include_outaged=False):
|
|
2778
|
+
"""返回单条支路(字典)或 None。section 为分段号过滤。"""
|
|
2779
|
+
for b in self.branches(include_outaged=include_outaged):
|
|
2780
|
+
if (b["bus1_name"].strip().upper() == bus1.strip().upper()
|
|
2781
|
+
and abs(b["kv1"] - kv1) < 0.005
|
|
2782
|
+
and b["bus2_name"].strip().upper() == bus2.strip().upper()
|
|
2783
|
+
and abs(b["kv2"] - kv2) < 0.005
|
|
2784
|
+
and (ckt is None or b["ckt"] == ckt)
|
|
2785
|
+
and (section is None
|
|
2786
|
+
or int(b.get("section", 0)) == int(section))):
|
|
2787
|
+
return b
|
|
2788
|
+
return None
|
|
2789
|
+
|
|
2790
|
+
def outaged_branches(self):
|
|
2791
|
+
"""返回本实例中通过 outage_branch 开断且尚未恢复的支路。"""
|
|
2792
|
+
return [b for b in self.branches(include_outaged=True)
|
|
2793
|
+
if not b.get("in_service", True)]
|
|
2794
|
+
|
|
2795
|
+
def _areas_all(self):
|
|
2796
|
+
if "areas_all" in self._cache:
|
|
2797
|
+
return self._cache["areas_all"]
|
|
2798
|
+
ci = self.case_info()
|
|
2799
|
+
n = int(ci.get("num_areas") or 0)
|
|
2800
|
+
maxarea = max(n * 2, 64)
|
|
2801
|
+
while True:
|
|
2802
|
+
fields = (c_double * (maxarea * 6))()
|
|
2803
|
+
names = ((c_char * 11) * maxarea)()
|
|
2804
|
+
sbuses = ((c_char * 9) * maxarea)()
|
|
2805
|
+
got = self._lib.ipfpy_get_areas(
|
|
2806
|
+
ctypes.byref(fields), ctypes.byref(names),
|
|
2807
|
+
ctypes.byref(sbuses), maxarea)
|
|
2808
|
+
if got >= 0:
|
|
2809
|
+
break
|
|
2810
|
+
if maxarea >= 10000:
|
|
2811
|
+
raise PyFlowError("ipfpy_get_areas 缓冲区不足")
|
|
2812
|
+
maxarea *= 2
|
|
2813
|
+
out = []
|
|
2814
|
+
f = fields
|
|
2815
|
+
for i in range(got):
|
|
2816
|
+
out.append({
|
|
2817
|
+
"name": _text(names[i].raw),
|
|
2818
|
+
"sbus_name": _text(sbuses[i].raw),
|
|
2819
|
+
"sched_export": f[i * 6 + 0],
|
|
2820
|
+
"Pgen": f[i * 6 + 1], "Pload": f[i * 6 + 2],
|
|
2821
|
+
"Ploss": f[i * 6 + 3], "Pexport": f[i * 6 + 4],
|
|
2822
|
+
"max_Vpu": f[i * 6 + 5],
|
|
2823
|
+
})
|
|
2824
|
+
self._cache["areas_all"] = out
|
|
2825
|
+
return out
|
|
2826
|
+
|
|
2827
|
+
def areas(self):
|
|
2828
|
+
"""返回控制区列表(含求解字段)。"""
|
|
2829
|
+
return [dict(a) for a in self._areas_all()]
|
|
2830
|
+
|
|
2831
|
+
def ities(self):
|
|
2832
|
+
"""返回联络线输入与求解数据。"""
|
|
2833
|
+
if "ities" in self._cache:
|
|
2834
|
+
return [dict(x) for x in self._cache["ities"]]
|
|
2835
|
+
if not hasattr(self._lib, "ipfpy_get_ities"):
|
|
2836
|
+
return []
|
|
2837
|
+
maxitie = 64
|
|
2838
|
+
while True:
|
|
2839
|
+
fields = (c_double * (maxitie * 4))()
|
|
2840
|
+
area1 = ((c_char * 11) * maxitie)()
|
|
2841
|
+
area2 = ((c_char * 11) * maxitie)()
|
|
2842
|
+
types = ((c_char * 3) * maxitie)()
|
|
2843
|
+
got = self._lib.ipfpy_get_ities(
|
|
2844
|
+
ctypes.byref(fields), ctypes.byref(area1),
|
|
2845
|
+
ctypes.byref(area2), ctypes.byref(types), maxitie)
|
|
2846
|
+
if got >= 0:
|
|
2847
|
+
break
|
|
2848
|
+
if maxitie >= MAXTIE * 2:
|
|
2849
|
+
raise PyFlowError("ipfpy_get_ities 缓冲区不足")
|
|
2850
|
+
maxitie *= 2
|
|
2851
|
+
out = []
|
|
2852
|
+
for i in range(got):
|
|
2853
|
+
out.append({
|
|
2854
|
+
"type": _text(types[i].raw),
|
|
2855
|
+
"area1": _text(area1[i].raw),
|
|
2856
|
+
"area2": _text(area2[i].raw),
|
|
2857
|
+
"sched_export": fields[i * 4 + 0],
|
|
2858
|
+
"Pexport": fields[i * 4 + 1],
|
|
2859
|
+
"Pcirc": fields[i * 4 + 2],
|
|
2860
|
+
"input_exists": bool(fields[i * 4 + 3]),
|
|
2861
|
+
})
|
|
2862
|
+
self._cache["ities"] = out
|
|
2863
|
+
return [dict(x) for x in out]
|
|
2864
|
+
|
|
2865
|
+
def cbuses(self):
|
|
2866
|
+
"""返回连续母线 + 记录数据。"""
|
|
2867
|
+
if "cbuses" in self._cache:
|
|
2868
|
+
return [dict(x) for x in self._cache["cbuses"]]
|
|
2869
|
+
if not hasattr(self._lib, "ipfpy_get_cbuses"):
|
|
2870
|
+
return []
|
|
2871
|
+
maxc = 256
|
|
2872
|
+
while True:
|
|
2873
|
+
fields = (c_double * (maxc * 13))()
|
|
2874
|
+
names = ((c_char * 9) * maxc)()
|
|
2875
|
+
types = ((c_char * 3) * maxc)()
|
|
2876
|
+
owners = ((c_char * 4) * maxc)()
|
|
2877
|
+
years = ((c_char * 3) * maxc)()
|
|
2878
|
+
kvs = (c_float * maxc)()
|
|
2879
|
+
got = self._lib.ipfpy_get_cbuses(
|
|
2880
|
+
ctypes.byref(fields), ctypes.byref(names),
|
|
2881
|
+
ctypes.byref(types), ctypes.byref(owners),
|
|
2882
|
+
ctypes.byref(years), ctypes.byref(kvs), maxc)
|
|
2883
|
+
if got >= 0:
|
|
2884
|
+
break
|
|
2885
|
+
if maxc >= MAXBUS:
|
|
2886
|
+
raise PyFlowError("ipfpy_get_cbuses 缓冲区不足")
|
|
2887
|
+
maxc *= 2
|
|
2888
|
+
out = []
|
|
2889
|
+
for i in range(got):
|
|
2890
|
+
out.append({
|
|
2891
|
+
"type": _text(types[i].raw),
|
|
2892
|
+
"owner": _text(owners[i].raw),
|
|
2893
|
+
"name": _text(names[i].raw),
|
|
2894
|
+
"kv": kvs[i],
|
|
2895
|
+
"year": _text(years[i].raw),
|
|
2896
|
+
"Pload": fields[i * 13 + 0],
|
|
2897
|
+
"Qload": fields[i * 13 + 1],
|
|
2898
|
+
"Gshunt": fields[i * 13 + 2],
|
|
2899
|
+
"Bshunt": fields[i * 13 + 3],
|
|
2900
|
+
"Pgen_sched": fields[i * 13 + 4],
|
|
2901
|
+
"Qgen_Qmax": fields[i * 13 + 5],
|
|
2902
|
+
"Qmin": fields[i * 13 + 6],
|
|
2903
|
+
"Pgen": fields[i * 13 + 7],
|
|
2904
|
+
"Qgen": fields[i * 13 + 8],
|
|
2905
|
+
"Pload_solved": fields[i * 13 + 9],
|
|
2906
|
+
"Qload_solved": fields[i * 13 + 10],
|
|
2907
|
+
"Gshunt_solved": fields[i * 13 + 11],
|
|
2908
|
+
"Bshunt_solved": fields[i * 13 + 12],
|
|
2909
|
+
})
|
|
2910
|
+
self._cache["cbuses"] = out
|
|
2911
|
+
return [dict(x) for x in out]
|
|
2912
|
+
|
|
2913
|
+
def qcurves(self):
|
|
2914
|
+
"""返回无功能力曲线原始点,p_levels 为 Pgen0..Pgen9。"""
|
|
2915
|
+
if "qcurves" in self._cache:
|
|
2916
|
+
return [dict(x) for x in self._cache["qcurves"]]
|
|
2917
|
+
if not hasattr(self._lib, "ipfpy_get_qcurves"):
|
|
2918
|
+
return []
|
|
2919
|
+
maxq = 256
|
|
2920
|
+
while True:
|
|
2921
|
+
fields = (c_double * (maxq * 10))()
|
|
2922
|
+
names = ((c_char * 9) * maxq)()
|
|
2923
|
+
types = ((c_char * 3) * maxq)()
|
|
2924
|
+
pu_codes = ((c_char * 3) * maxq)()
|
|
2925
|
+
actives = ((c_char * 1) * maxq)()
|
|
2926
|
+
kvs = (c_float * maxq)()
|
|
2927
|
+
got = self._lib.ipfpy_get_qcurves(
|
|
2928
|
+
ctypes.byref(fields), ctypes.byref(names),
|
|
2929
|
+
ctypes.byref(types), ctypes.byref(pu_codes),
|
|
2930
|
+
ctypes.byref(actives), ctypes.byref(kvs), maxq)
|
|
2931
|
+
if got >= 0:
|
|
2932
|
+
break
|
|
2933
|
+
if maxq >= MAXBUS:
|
|
2934
|
+
raise PyFlowError("ipfpy_get_qcurves 缓冲区不足")
|
|
2935
|
+
maxq *= 2
|
|
2936
|
+
out = []
|
|
2937
|
+
for i in range(got):
|
|
2938
|
+
out.append({
|
|
2939
|
+
"type": _text(types[i].raw),
|
|
2940
|
+
"pu_code": _text(pu_codes[i].raw),
|
|
2941
|
+
"active": actives.raw[i:i + 1].decode(
|
|
2942
|
+
"ascii", "replace"),
|
|
2943
|
+
"bus": _text(names[i].raw),
|
|
2944
|
+
"kv": kvs[i],
|
|
2945
|
+
"p_levels": [fields[i * 10 + j] for j in range(10)],
|
|
2946
|
+
})
|
|
2947
|
+
self._cache["qcurves"] = out
|
|
2948
|
+
return [dict(x) for x in out]
|
|
2949
|
+
|
|
2950
|
+
def xdata(self):
|
|
2951
|
+
"""返回可投切电抗器各段计划/实际步数与 MVAr。"""
|
|
2952
|
+
if "xdata" in self._cache:
|
|
2953
|
+
return [dict(x) for x in self._cache["xdata"]]
|
|
2954
|
+
if not hasattr(self._lib, "ipfpy_get_xdata"):
|
|
2955
|
+
return []
|
|
2956
|
+
maxx = 256
|
|
2957
|
+
while True:
|
|
2958
|
+
fields = (c_double * (maxx * 40))()
|
|
2959
|
+
names = ((c_char * 9) * maxx)()
|
|
2960
|
+
owners = ((c_char * 4) * maxx)()
|
|
2961
|
+
types = ((c_char * 3) * maxx)()
|
|
2962
|
+
rmt_names = ((c_char * 9) * maxx)()
|
|
2963
|
+
kvs = (c_float * maxx)()
|
|
2964
|
+
rmt_kvs = (c_float * maxx)()
|
|
2965
|
+
got = self._lib.ipfpy_get_xdata(
|
|
2966
|
+
ctypes.byref(fields), ctypes.byref(names),
|
|
2967
|
+
ctypes.byref(owners), ctypes.byref(types),
|
|
2968
|
+
ctypes.byref(rmt_names), ctypes.byref(kvs),
|
|
2969
|
+
ctypes.byref(rmt_kvs), maxx)
|
|
2970
|
+
if got >= 0:
|
|
2971
|
+
break
|
|
2972
|
+
if maxx >= MAXBUS:
|
|
2973
|
+
raise PyFlowError("ipfpy_get_xdata 缓冲区不足")
|
|
2974
|
+
maxx *= 2
|
|
2975
|
+
out = []
|
|
2976
|
+
for i in range(got):
|
|
2977
|
+
base = i * 40
|
|
2978
|
+
segments = []
|
|
2979
|
+
for j in range(8):
|
|
2980
|
+
segments.append({
|
|
2981
|
+
"num_steps": int(fields[base + j]),
|
|
2982
|
+
"delta_mva": fields[base + 8 + j],
|
|
2983
|
+
"sch_units": int(fields[base + 16 + j]),
|
|
2984
|
+
"used_units": int(fields[base + 24 + j]),
|
|
2985
|
+
"mvar_per_unit": fields[base + 32 + j],
|
|
2986
|
+
})
|
|
2987
|
+
out.append({
|
|
2988
|
+
"type": _text(types[i].raw),
|
|
2989
|
+
"owner": _text(owners[i].raw),
|
|
2990
|
+
"bus": _text(names[i].raw),
|
|
2991
|
+
"kv": kvs[i],
|
|
2992
|
+
"remote_bus": _text(rmt_names[i].raw),
|
|
2993
|
+
"remote_kv": rmt_kvs[i],
|
|
2994
|
+
"segments": segments,
|
|
2995
|
+
})
|
|
2996
|
+
self._cache["xdata"] = out
|
|
2997
|
+
return [dict(x) for x in out]
|
|
2998
|
+
|
|
2999
|
+
def comments(self):
|
|
3000
|
+
"""返回算例名、描述、三行标题和最多 20 行注释。"""
|
|
3001
|
+
if "comments" in self._cache:
|
|
3002
|
+
return dict(self._cache["comments"])
|
|
3003
|
+
if not hasattr(self._lib, "ipfpy_get_comments"):
|
|
3004
|
+
return {"case_name": "", "case_desc": "", "headers": [],
|
|
3005
|
+
"comments": []}
|
|
3006
|
+
case_name = create_string_buffer(11)
|
|
3007
|
+
case_desc = create_string_buffer(21)
|
|
3008
|
+
headers = create_string_buffer(3 * 133)
|
|
3009
|
+
comments = create_string_buffer(20 * 121)
|
|
3010
|
+
rc = self._lib.ipfpy_get_comments(case_name, case_desc,
|
|
3011
|
+
headers, comments)
|
|
3012
|
+
if rc != 0:
|
|
3013
|
+
raise PyFlowError("ipfpy_get_comments 失败, rc=%d" % rc)
|
|
3014
|
+
|
|
3015
|
+
def _fixed(raw):
|
|
3016
|
+
return bytes(raw).decode("ascii", "replace").rstrip(
|
|
3017
|
+
"\x00 ").strip()
|
|
3018
|
+
|
|
3019
|
+
out = {
|
|
3020
|
+
"case_name": _fixed(case_name.raw),
|
|
3021
|
+
"case_desc": _fixed(case_desc.raw),
|
|
3022
|
+
"headers": [_fixed(headers.raw[i * 133:(i + 1) * 133])
|
|
3023
|
+
for i in range(3)],
|
|
3024
|
+
"comments": [_fixed(comments.raw[i * 121:(i + 1) * 121])
|
|
3025
|
+
for i in range(20)],
|
|
3026
|
+
}
|
|
3027
|
+
self._cache["comments"] = out
|
|
3028
|
+
return dict(out)
|
|
3029
|
+
|
|
3030
|
+
# 兼容旧方案文档命名的别名
|
|
3031
|
+
get_buses = buses
|
|
3032
|
+
get_branches = branches
|
|
3033
|
+
get_areas = areas
|
|
3034
|
+
|
|
3035
|
+
# ------------------------------------------------------------------
|
|
3036
|
+
# pandas 集成(可选依赖;未安装时回退 list[dict],pd.DataFrame 可直接转)
|
|
3037
|
+
# ------------------------------------------------------------------
|
|
3038
|
+
@staticmethod
|
|
3039
|
+
def _require_pandas():
|
|
3040
|
+
try:
|
|
3041
|
+
import pandas as pd
|
|
3042
|
+
except ImportError:
|
|
3043
|
+
raise PyFlowError(
|
|
3044
|
+
"pandas 未安装;可 pip install pandas,或直接对 "
|
|
3045
|
+
"buses()/branches()/areas() 返回的 list[dict] 调 "
|
|
3046
|
+
"pd.DataFrame(...)")
|
|
3047
|
+
return pd
|
|
3048
|
+
|
|
3049
|
+
def buses_df(self):
|
|
3050
|
+
"""母线数据 -> pandas.DataFrame。"""
|
|
3051
|
+
return self._require_pandas().DataFrame(self.buses())
|
|
3052
|
+
|
|
3053
|
+
def branches_df(self):
|
|
3054
|
+
"""支路数据 -> pandas.DataFrame。"""
|
|
3055
|
+
return self._require_pandas().DataFrame(self.branches())
|
|
3056
|
+
|
|
3057
|
+
def areas_df(self):
|
|
3058
|
+
"""控制区数据 -> pandas.DataFrame。"""
|
|
3059
|
+
return self._require_pandas().DataFrame(self.areas())
|
|
3060
|
+
|
|
3061
|
+
# ------------------------------------------------------------------
|
|
3062
|
+
# 内部计算数据
|
|
3063
|
+
# ------------------------------------------------------------------
|
|
3064
|
+
def y_matrix(self):
|
|
3065
|
+
"""导纳矩阵(稀疏 CSR,母线按输入(外部)序排列)。
|
|
3066
|
+
|
|
3067
|
+
/alpha/ 中 km/kmlen 是"母线号 -> 行池起点/行长"的散列指针,
|
|
3068
|
+
行池顺序为内部(最优)序。此处按母线输入序逐行 scatter 重建。
|
|
3069
|
+
|
|
3070
|
+
返回字典:
|
|
3071
|
+
nbus 母线数(含平衡机)
|
|
3072
|
+
nnz 非对角元素个数
|
|
3073
|
+
indptr, indices 0-based CSR(scipy 兼容)
|
|
3074
|
+
G, B 非对角元素实部/虚部
|
|
3075
|
+
Gdiag, Bdiag 对角元素实部/虚部(母线输入序)
|
|
3076
|
+
"""
|
|
3077
|
+
nbus = c_int()
|
|
3078
|
+
nnz = c_int()
|
|
3079
|
+
iord = c_int()
|
|
3080
|
+
self._lib.ipfpy_y_info(byref(nbus), byref(nnz), byref(iord))
|
|
3081
|
+
|
|
3082
|
+
n = nbus.value
|
|
3083
|
+
m = nnz.value
|
|
3084
|
+
km = (c_int * max(n, 1))()
|
|
3085
|
+
kmlen = (c_int * max(n, 1))()
|
|
3086
|
+
ikmu = (c_int * max(m, 1))()
|
|
3087
|
+
gkmu = (c_double * max(m, 1))()
|
|
3088
|
+
bkmu = (c_double * max(m, 1))()
|
|
3089
|
+
gkku = (c_double * max(n, 1))()
|
|
3090
|
+
bkku = (c_double * max(n, 1))()
|
|
3091
|
+
self._lib.ipfpy_y_data(
|
|
3092
|
+
km, kmlen, ikmu, gkmu, bkmu, gkku, bkku,
|
|
3093
|
+
byref(nbus), byref(nnz))
|
|
3094
|
+
|
|
3095
|
+
indptr, indices, g, b = self._y_build_csr(
|
|
3096
|
+
km, kmlen, ikmu, gkmu, bkmu, n, m, iord.value)
|
|
3097
|
+
gdiag = _arr(gkku, n)
|
|
3098
|
+
bdiag = _arr(bkku, n)
|
|
3099
|
+
# 当 ordymx=2 时,gkku/bkku 为最优序,indices 已转为输入序,
|
|
3100
|
+
# 此处将 gdiag/bdiag 也转为输入序以保持一致
|
|
3101
|
+
if iord.value == 2:
|
|
3102
|
+
inp2opt = self._mapping_arrays()[0]
|
|
3103
|
+
if _np is not None:
|
|
3104
|
+
inp2opt_0 = _np.asarray(inp2opt, dtype=_np.int64) - 1
|
|
3105
|
+
gdiag = _np.asarray(gdiag, dtype=_np.float64)
|
|
3106
|
+
bdiag = _np.asarray(bdiag, dtype=_np.float64)
|
|
3107
|
+
gdiag = gdiag[inp2opt_0]
|
|
3108
|
+
bdiag = bdiag[inp2opt_0]
|
|
3109
|
+
else:
|
|
3110
|
+
gdiag = [gdiag[int(j) - 1] for j in inp2opt]
|
|
3111
|
+
bdiag = [bdiag[int(j) - 1] for j in inp2opt]
|
|
3112
|
+
|
|
3113
|
+
return {
|
|
3114
|
+
"nbus": n,
|
|
3115
|
+
"nnz": m,
|
|
3116
|
+
"ordymx": iord.value,
|
|
3117
|
+
"order": "input",
|
|
3118
|
+
"indptr": indptr,
|
|
3119
|
+
"indices": indices,
|
|
3120
|
+
"G": g,
|
|
3121
|
+
"B": b,
|
|
3122
|
+
"Gdiag": gdiag,
|
|
3123
|
+
"Bdiag": bdiag,
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
def _y_build_csr(self, km, kmlen, ikmu, gkmu, bkmu, n, m, iord):
|
|
3127
|
+
"""把 /alpha/ 行池按母线输入序 scatter 为 0-based CSR。
|
|
3128
|
+
|
|
3129
|
+
``ordymx==2`` 时 km/kmlen 的行号和 ikmu 的列号都是内部最优序;
|
|
3130
|
+
通过 inp2opt/opt2inp 同时转换行、列,输出始终为输入序。
|
|
3131
|
+
"""
|
|
3132
|
+
inp2opt, opt2inp = self._mapping_arrays()
|
|
3133
|
+
if iord != 2:
|
|
3134
|
+
inp2opt = list(range(1, n + 1))
|
|
3135
|
+
opt2inp = list(range(1, n + 1))
|
|
3136
|
+
if _np is None:
|
|
3137
|
+
km_a = _arr(km, n)
|
|
3138
|
+
kmlen_a = _arr(kmlen, n)
|
|
3139
|
+
ikmu_a = _arr(ikmu, m)
|
|
3140
|
+
g_a = _arr(gkmu, m)
|
|
3141
|
+
b_a = _arr(bkmu, m)
|
|
3142
|
+
indptr = [0]
|
|
3143
|
+
indices = []
|
|
3144
|
+
g = []
|
|
3145
|
+
b = []
|
|
3146
|
+
for input_i in range(n):
|
|
3147
|
+
opt_i = int(inp2opt[input_i]) - 1
|
|
3148
|
+
ln = kmlen_a[opt_i]
|
|
3149
|
+
s = km_a[opt_i] - 1
|
|
3150
|
+
indptr.append(indptr[-1] + ln)
|
|
3151
|
+
for j in range(ln):
|
|
3152
|
+
indices.append(opt2inp[ikmu_a[s + j] - 1] - 1)
|
|
3153
|
+
g.append(g_a[s + j])
|
|
3154
|
+
b.append(b_a[s + j])
|
|
3155
|
+
return indptr, indices, g, b
|
|
3156
|
+
km_a = _np.asarray(_arr(km, n, _np.int32), dtype=_np.int64)
|
|
3157
|
+
kmlen_a = _np.asarray(_arr(kmlen, n, _np.int32), dtype=_np.int64)
|
|
3158
|
+
ikmu_a = _np.asarray(_arr(ikmu, m, _np.int32), dtype=_np.int64)
|
|
3159
|
+
g_a = _np.asarray(_arr(gkmu, m))
|
|
3160
|
+
b_a = _np.asarray(_arr(bkmu, m))
|
|
3161
|
+
inp2opt_a = _np.asarray(inp2opt, dtype=_np.int64)
|
|
3162
|
+
opt2inp_a = _np.asarray(opt2inp, dtype=_np.int64)
|
|
3163
|
+
if n > 0:
|
|
3164
|
+
opt_rows = inp2opt_a - 1
|
|
3165
|
+
pool_pos = _np.concatenate([
|
|
3166
|
+
km_a[i] - 1 + _np.arange(kmlen_a[i])
|
|
3167
|
+
for i in opt_rows])
|
|
3168
|
+
indices = (opt2inp_a[ikmu_a[pool_pos] - 1] - 1).astype(
|
|
3169
|
+
_np.int32)
|
|
3170
|
+
indptr = _np.zeros(n + 1, dtype=_np.int64)
|
|
3171
|
+
indptr[1:] = _np.cumsum(kmlen_a[opt_rows])
|
|
3172
|
+
return indptr, indices, g_a[pool_pos], b_a[pool_pos]
|
|
3173
|
+
return (_np.zeros(1, dtype=_np.int64), _np.array([],
|
|
3174
|
+
dtype=_np.int32), _np.array([]), _np.array([]))
|
|
3175
|
+
def _mapping_arrays(self):
|
|
3176
|
+
"""内外序映射(voltages() 内缓存)。"""
|
|
3177
|
+
if not hasattr(self, "_map"):
|
|
3178
|
+
self.voltages()
|
|
3179
|
+
return self._inp2opt, self._opt2inp
|
|
3180
|
+
|
|
3181
|
+
def _iteration_info(self, require_valid=True):
|
|
3182
|
+
values = [c_int() for _ in range(7)]
|
|
3183
|
+
base_mva = c_float()
|
|
3184
|
+
self._lib.ipfpy_iteration_info(
|
|
3185
|
+
*[byref(v) for v in values], byref(base_mva))
|
|
3186
|
+
info = {
|
|
3187
|
+
"valid": bool(values[0].value),
|
|
3188
|
+
"iteration": values[1].value,
|
|
3189
|
+
"nbus": values[2].value,
|
|
3190
|
+
"nblock": values[3].value,
|
|
3191
|
+
"nnz_blocks": values[4].value,
|
|
3192
|
+
"overflow": bool(values[5].value),
|
|
3193
|
+
"nltc": values[6].value,
|
|
3194
|
+
"base_mva": base_mva.value,
|
|
3195
|
+
}
|
|
3196
|
+
if require_valid and not info["valid"]:
|
|
3197
|
+
raise PyFlowError(
|
|
3198
|
+
"尚无有效迭代快照;请先 start_solution() 并调用 "
|
|
3199
|
+
"step_solution()")
|
|
3200
|
+
return info
|
|
3201
|
+
|
|
3202
|
+
def iteration_jacobian(self, format="csr"):
|
|
3203
|
+
"""返回本次迭代消元前的原始 Newton 雅可比。
|
|
3204
|
+
|
|
3205
|
+
``format='bsr'`` 返回 2x2 块稀疏矩阵;``'csr'`` 将其无损展开为
|
|
3206
|
+
标准标量 CSR。行方程为 ``[P, Q/电压控制]``,列变量为
|
|
3207
|
+
``[angle_rad, relative_voltage]``;前 ``nltc`` 个块是 LTC 控制
|
|
3208
|
+
变量/方程,随后为按引擎最优序排列的母线块。
|
|
3209
|
+
"""
|
|
3210
|
+
info = self._iteration_info()
|
|
3211
|
+
nblock, nnz = info["nblock"], info["nnz_blocks"]
|
|
3212
|
+
rowmax, nnzmax = c_int(nblock + 1), c_int(nnz)
|
|
3213
|
+
rowptr_c = (c_int * (nblock + 1))()
|
|
3214
|
+
colind_c = (c_int * max(nnz, 1))()
|
|
3215
|
+
jh = (c_double * max(nnz, 1))()
|
|
3216
|
+
jn = (c_double * max(nnz, 1))()
|
|
3217
|
+
jj = (c_double * max(nnz, 1))()
|
|
3218
|
+
jl = (c_double * max(nnz, 1))()
|
|
3219
|
+
rhs1 = (c_double * max(nblock, 1))()
|
|
3220
|
+
rhs2 = (c_double * max(nblock, 1))()
|
|
3221
|
+
self._lib.ipfpy_iteration_jac(
|
|
3222
|
+
byref(rowmax), byref(nnzmax), rowptr_c, colind_c,
|
|
3223
|
+
jh, jn, jj, jl, rhs1, rhs2)
|
|
3224
|
+
rowptr = _arr(rowptr_c, nblock + 1,
|
|
3225
|
+
_np.int32 if _np is not None else None)
|
|
3226
|
+
indices = _arr(colind_c, nnz,
|
|
3227
|
+
_np.int32 if _np is not None else None)
|
|
3228
|
+
if _np is not None:
|
|
3229
|
+
rowptr = rowptr.astype(_np.int64) - 1
|
|
3230
|
+
indices = indices - 1
|
|
3231
|
+
blocks = _np.empty((nnz, 2, 2), dtype=_np.float64)
|
|
3232
|
+
blocks[:, 0, 0] = _arr(jh, nnz)
|
|
3233
|
+
blocks[:, 0, 1] = _arr(jn, nnz)
|
|
3234
|
+
blocks[:, 1, 0] = _arr(jj, nnz)
|
|
3235
|
+
blocks[:, 1, 1] = _arr(jl, nnz)
|
|
3236
|
+
rhs = _np.empty(2 * nblock, dtype=_np.float64)
|
|
3237
|
+
rhs[0::2] = _arr(rhs1, nblock)
|
|
3238
|
+
rhs[1::2] = _arr(rhs2, nblock)
|
|
3239
|
+
else:
|
|
3240
|
+
rowptr = [v - 1 for v in rowptr]
|
|
3241
|
+
indices = [v - 1 for v in indices]
|
|
3242
|
+
blocks = [[[jh[k], jn[k]], [jj[k], jl[k]]]
|
|
3243
|
+
for k in range(nnz)]
|
|
3244
|
+
rhs = [value for i in range(nblock)
|
|
3245
|
+
for value in (rhs1[i], rhs2[i])]
|
|
3246
|
+
|
|
3247
|
+
common = {
|
|
3248
|
+
"iteration": info["iteration"], "nblock": nblock,
|
|
3249
|
+
"nltc": info["nltc"], "nbus": info["nbus"],
|
|
3250
|
+
"rhs": rhs, "base_mva": info["base_mva"],
|
|
3251
|
+
"equation_order": ("active_power", "reactive_or_control"),
|
|
3252
|
+
"variable_order": ("angle_rad", "relative_voltage"),
|
|
3253
|
+
"linear_system": "J * correction = rhs",
|
|
3254
|
+
"phase": "pre_update",
|
|
3255
|
+
}
|
|
3256
|
+
if format.lower() == "bsr":
|
|
3257
|
+
common.update({
|
|
3258
|
+
"format": "bsr", "shape": (2 * nblock, 2 * nblock),
|
|
3259
|
+
"blocksize": (2, 2), "indptr": rowptr,
|
|
3260
|
+
"indices": indices, "data": blocks,
|
|
3261
|
+
})
|
|
3262
|
+
return common
|
|
3263
|
+
if format.lower() != "csr":
|
|
3264
|
+
raise ValueError("format 仅支持 'csr' 或 'bsr'")
|
|
3265
|
+
|
|
3266
|
+
scalar_ptr, scalar_col, scalar_data = [0], [], []
|
|
3267
|
+
for block_row in range(nblock):
|
|
3268
|
+
for local_row in range(2):
|
|
3269
|
+
for pos in range(int(rowptr[block_row]),
|
|
3270
|
+
int(rowptr[block_row + 1])):
|
|
3271
|
+
block_col = int(indices[pos])
|
|
3272
|
+
for local_col in range(2):
|
|
3273
|
+
scalar_col.append(2 * block_col + local_col)
|
|
3274
|
+
scalar_data.append(float(blocks[pos][local_row][local_col]))
|
|
3275
|
+
scalar_ptr.append(len(scalar_col))
|
|
3276
|
+
if _np is not None:
|
|
3277
|
+
scalar_ptr = _np.asarray(scalar_ptr, dtype=_np.int64)
|
|
3278
|
+
scalar_col = _np.asarray(scalar_col, dtype=_np.int32)
|
|
3279
|
+
scalar_data = _np.asarray(scalar_data, dtype=_np.float64)
|
|
3280
|
+
common.update({
|
|
3281
|
+
"format": "csr", "shape": (2 * nblock, 2 * nblock),
|
|
3282
|
+
"indptr": scalar_ptr, "indices": scalar_col,
|
|
3283
|
+
"data": scalar_data,
|
|
3284
|
+
})
|
|
3285
|
+
return common
|
|
3286
|
+
|
|
3287
|
+
def iteration_snapshot(self, include_jacobian=True,
|
|
3288
|
+
jacobian_format="csr"):
|
|
3289
|
+
"""返回同一 Newton 迭代相位的一致状态快照。
|
|
3290
|
+
|
|
3291
|
+
快照包含更新前/后电压、原始节点不平衡量、实际施加修正量、
|
|
3292
|
+
当前 PV/PQ 控制映射;可选附带消元前雅可比。所有母线向量均按
|
|
3293
|
+
输入顺序排列。
|
|
3294
|
+
"""
|
|
3295
|
+
info = self._iteration_info()
|
|
3296
|
+
n = info["nbus"]
|
|
3297
|
+
arrays = [(c_double * max(n, 1))() for _ in range(6)]
|
|
3298
|
+
mode = (c_int * max(n, 1))()
|
|
3299
|
+
inp2opt = (c_int * max(n, 1))()
|
|
3300
|
+
opt2inp = (c_int * max(n, 1))()
|
|
3301
|
+
nmax = c_int(n)
|
|
3302
|
+
self._lib.ipfpy_iteration_bus(
|
|
3303
|
+
byref(nmax), *arrays, mode, inp2opt, opt2inp)
|
|
3304
|
+
pre_e, pre_f, post_e, post_f, dtheta, dvrel = [
|
|
3305
|
+
_arr(a, n) for a in arrays]
|
|
3306
|
+
mode_a = _arr(mode, n, _np.int32 if _np is not None else None)
|
|
3307
|
+
inp_a = _arr(inp2opt, n, _np.int32 if _np is not None else None)
|
|
3308
|
+
opt_a = _arr(opt2inp, n, _np.int32 if _np is not None else None)
|
|
3309
|
+
vd = self.voltages()
|
|
3310
|
+
names = vd["names"][:n]
|
|
3311
|
+
bases = vd["base"][:n]
|
|
3312
|
+
mismatch = self.mismatch_bus()
|
|
3313
|
+
|
|
3314
|
+
if _np is not None:
|
|
3315
|
+
pre_mag = _np.hypot(pre_e, pre_f)
|
|
3316
|
+
post_mag = _np.hypot(post_e, post_f)
|
|
3317
|
+
pre_ang = _np.arctan2(pre_f, pre_e)
|
|
3318
|
+
post_ang = _np.arctan2(post_f, post_e)
|
|
3319
|
+
else:
|
|
3320
|
+
import math
|
|
3321
|
+
pre_mag = [math.hypot(pre_e[i], pre_f[i]) for i in range(n)]
|
|
3322
|
+
post_mag = [math.hypot(post_e[i], post_f[i]) for i in range(n)]
|
|
3323
|
+
pre_ang = [math.atan2(pre_f[i], pre_e[i]) for i in range(n)]
|
|
3324
|
+
post_ang = [math.atan2(post_f[i], post_e[i]) for i in range(n)]
|
|
3325
|
+
result = dict(info)
|
|
3326
|
+
result.update({
|
|
3327
|
+
"phase_contract": {
|
|
3328
|
+
"voltage_before": "x_k",
|
|
3329
|
+
"mismatch": "F(x_k)",
|
|
3330
|
+
"jacobian": "J(x_k)",
|
|
3331
|
+
"correction": "applied_delta_x_k",
|
|
3332
|
+
"voltage_after": "x_k_plus_1",
|
|
3333
|
+
},
|
|
3334
|
+
"bus_order": "input", "names": names, "base_kv": bases,
|
|
3335
|
+
"voltage_before": {"e": pre_e, "f": pre_f,
|
|
3336
|
+
"magnitude_pu": pre_mag,
|
|
3337
|
+
"angle_rad": pre_ang},
|
|
3338
|
+
"voltage_after": {"e": post_e, "f": post_f,
|
|
3339
|
+
"magnitude_pu": post_mag,
|
|
3340
|
+
"angle_rad": post_ang},
|
|
3341
|
+
"correction": {"angle_rad": dtheta,
|
|
3342
|
+
"relative_voltage": dvrel},
|
|
3343
|
+
"control_bus_opt_index": mode_a,
|
|
3344
|
+
"inp2opt": inp_a, "opt2inp": opt_a,
|
|
3345
|
+
"bus_mismatch": mismatch,
|
|
3346
|
+
})
|
|
3347
|
+
if include_jacobian:
|
|
3348
|
+
result["jacobian"] = self.iteration_jacobian(jacobian_format)
|
|
3349
|
+
return result
|
|
3350
|
+
|
|
3351
|
+
def jacobian(self):
|
|
3352
|
+
"""因子化雅可比矩阵数据。
|
|
3353
|
+
|
|
3354
|
+
返回字典:
|
|
3355
|
+
ntotx 变量数(母线数 + LTC 数)
|
|
3356
|
+
koptsw 求解选项(>2 时 amtrx 含完整因子化矩阵)
|
|
3357
|
+
jaclen amtrx 实际元素个数(jndex[1,ntotx]-1)
|
|
3358
|
+
jndex (2,ntotx) 行指针(jndex[1,row] 为 1-based 起始位置)
|
|
3359
|
+
amtrx 因子化矩阵元素(5 字/非对角,见文档)
|
|
3360
|
+
dpt (2,ntotx) 每次迭代残差(dpt[1,row]=P 偏差, dpt[2,row]=Q 偏差)
|
|
3361
|
+
"""
|
|
3362
|
+
ntotx = c_int()
|
|
3363
|
+
koptsw = c_int()
|
|
3364
|
+
jaclen = c_int()
|
|
3365
|
+
jndex = (c_int * (2 * (MAXLTC + MAXBUS)))()
|
|
3366
|
+
amtrx = (c_double * MAXMTX)()
|
|
3367
|
+
dpt = (c_double * (2 * (MAXBUS + 2 * MAXLTC)))()
|
|
3368
|
+
self._lib.ipfpy_jac_data(jndex, amtrx, dpt, byref(jaclen),
|
|
3369
|
+
byref(ntotx), byref(koptsw))
|
|
3370
|
+
n = ntotx.value
|
|
3371
|
+
jl = max(jaclen.value, 0)
|
|
3372
|
+
return {
|
|
3373
|
+
"ntotx": n,
|
|
3374
|
+
"koptsw": koptsw.value,
|
|
3375
|
+
"jaclen": jl,
|
|
3376
|
+
"jndex": _arr(jndex, 2 * n, _np.int32 if _np else None),
|
|
3377
|
+
"amtrx": _arr(amtrx, jl),
|
|
3378
|
+
"dpt": _arr(dpt, 2 * n),
|
|
3379
|
+
}
|
|
3380
|
+
def mismatch(self):
|
|
3381
|
+
"""求解后偏差汇总(/ecvar/ + /itrhis/)。
|
|
3382
|
+
|
|
3383
|
+
返回字典:
|
|
3384
|
+
dptot/dqtot/dttot/datot/ddtot 总体 P/Q/总和/角度 偏差
|
|
3385
|
+
kownt..kowntd 各类约束计数
|
|
3386
|
+
ittot 当前迭代计数
|
|
3387
|
+
itrtyp 1=解耦法 2=Newton-Raphson
|
|
3388
|
+
itrhis[1/2] 解耦/NR 迭代次数
|
|
3389
|
+
ntot 母线数
|
|
3390
|
+
"""
|
|
3391
|
+
dp, dq, dt, da, dd = (c_double() for _ in range(5))
|
|
3392
|
+
kw, kwa, kwb, kwc, kwd = (c_int() for _ in range(5))
|
|
3393
|
+
it, typ, rh1, rh2, ntot = (c_int() for _ in range(5))
|
|
3394
|
+
|
|
3395
|
+
self._lib.ipfpy_mismatch(
|
|
3396
|
+
byref(dp), byref(dq), byref(dt), byref(da), byref(dd),
|
|
3397
|
+
byref(kw), byref(kwa), byref(kwb), byref(kwc), byref(kwd),
|
|
3398
|
+
byref(it), byref(typ), byref(rh1), byref(rh2), byref(ntot))
|
|
3399
|
+
return {
|
|
3400
|
+
"dptot": dp.value, "dqtot": dq.value, "dttot": dt.value,
|
|
3401
|
+
"datot": da.value, "ddtot": dd.value,
|
|
3402
|
+
"kownt": kw.value, "kownta": kwa.value, "kowntb": kwb.value,
|
|
3403
|
+
"kowntc": kwc.value, "kowntd": kwd.value,
|
|
3404
|
+
"ittot": it.value, "itrtyp": typ.value,
|
|
3405
|
+
"itrhis_decoupled": rh1.value, "itrhis_nr": rh2.value,
|
|
3406
|
+
"ntot": ntot.value,
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
def mismatch_bus(self):
|
|
3410
|
+
"""返回逐节点不平衡功率(MW/MVAr)。
|
|
3411
|
+
|
|
3412
|
+
读取本次 NR 迭代中 ``jacbus`` 形成、且尚未被稀疏消元修改的
|
|
3413
|
+
母线方程残差,单位为 MW/MVAr。P 残差为计划注入减网络注入;
|
|
3414
|
+
PV/受控母线的第二方程是电压约束,因此 ``dq_mvar`` 返回 0。
|
|
3415
|
+
|
|
3416
|
+
Returns
|
|
3417
|
+
-------
|
|
3418
|
+
list[dict]
|
|
3419
|
+
[{"name": str, "kv": float, "dp_mw": float, "dq_mvar": float}, ...]
|
|
3420
|
+
"""
|
|
3421
|
+
nbus = c_int()
|
|
3422
|
+
maxbus = self.case_info().get("num_buses", 2000) + 100
|
|
3423
|
+
cbus = (c_char * (8 * maxbus))()
|
|
3424
|
+
p_mismatch = (c_double * maxbus)()
|
|
3425
|
+
q_mismatch = (c_double * maxbus)()
|
|
3426
|
+
base_mva = c_float()
|
|
3427
|
+
self._lib.ipfpy_mismatch_bus(
|
|
3428
|
+
byref(nbus), cbus, p_mismatch, q_mismatch, byref(base_mva))
|
|
3429
|
+
n = nbus.value
|
|
3430
|
+
out = []
|
|
3431
|
+
vd = self.voltages()
|
|
3432
|
+
for i in range(n):
|
|
3433
|
+
name = _text(bytes(cbus[i * 8:(i + 1) * 8]))
|
|
3434
|
+
out.append({
|
|
3435
|
+
"name": name,
|
|
3436
|
+
"kv": vd["base"][i] if i < len(vd["base"]) else 0.0,
|
|
3437
|
+
"dp_mw": p_mismatch[i],
|
|
3438
|
+
"dq_mvar": q_mismatch[i],
|
|
3439
|
+
})
|
|
3440
|
+
return out
|
|
3441
|
+
|
|
3442
|
+
def iteration_history(self):
|
|
3443
|
+
"""每次迭代误差最大的 8 条母线(/errbus/ 历史)。
|
|
3444
|
+
|
|
3445
|
+
返回列表,每项: {"iter": k, "top": [{"bus": 母线序号(1-based),
|
|
3446
|
+
"perr": 误差平方和, "dp": P 偏差, "dq": Q 偏差}, ...]};
|
|
3447
|
+
另有 "buscor" 键存放第二条统计(i=5 行)。
|
|
3448
|
+
"""
|
|
3449
|
+
buser = (c_float * (8 * 8 * 100))()
|
|
3450
|
+
ibuser = (c_int * (8 * 8 * 100))()
|
|
3451
|
+
self._lib.ipfpy_errbus(buser, ibuser)
|
|
3452
|
+
m = self.mismatch()
|
|
3453
|
+
# /errbus/ third dimension is fixed at 100 in the Fortran COMMON
|
|
3454
|
+
# block; clamp defensively if callers configure a larger iteration cap.
|
|
3455
|
+
niter = min(m["ittot"] + m["itrhis_decoupled"], 100)
|
|
3456
|
+
out = []
|
|
3457
|
+
for k in range(niter):
|
|
3458
|
+
top, buscor = [], []
|
|
3459
|
+
for i in range(1, 5):
|
|
3460
|
+
base = ((i - 1) * 100 + k) * 8
|
|
3461
|
+
b = ibuser[base + 0]
|
|
3462
|
+
if b > 0:
|
|
3463
|
+
top.append({
|
|
3464
|
+
"bus": b,
|
|
3465
|
+
"perr": buser[base + 1],
|
|
3466
|
+
"dp": buser[base + 2],
|
|
3467
|
+
"dq": buser[base + 3],
|
|
3468
|
+
})
|
|
3469
|
+
for i in range(5, 9):
|
|
3470
|
+
base = ((i - 1) * 100 + k) * 8
|
|
3471
|
+
b = ibuser[base + 0]
|
|
3472
|
+
if b > 0:
|
|
3473
|
+
buscor.append({
|
|
3474
|
+
"bus": b,
|
|
3475
|
+
"perr": buser[base + 1],
|
|
3476
|
+
"dp": buser[base + 2],
|
|
3477
|
+
"dq": buser[base + 3],
|
|
3478
|
+
})
|
|
3479
|
+
out.append({"iter": k + 1, "top": top, "buscor": buscor})
|
|
3480
|
+
return out
|
|
3481
|
+
|
|
3482
|
+
def voltages(self):
|
|
3483
|
+
"""电压向量与内外序映射(/bus_x/ + /bus_c/)。
|
|
3484
|
+
|
|
3485
|
+
返回字典:
|
|
3486
|
+
nbus 母线数
|
|
3487
|
+
ordvlt 1=外部序 / 2=内部最优序(内核原始数组的排序)
|
|
3488
|
+
e, f 电压实部/虚部(已统一转换为输入序,pu)
|
|
3489
|
+
e_internal/f_internal 内核原始顺序,供数值内核诊断
|
|
3490
|
+
base 基准电压(kV,外部序)
|
|
3491
|
+
names/zone/owner 母线名(外部序,8/2/3 字符)
|
|
3492
|
+
inp2opt, opt2inp 外部<->内部序映射(1-based)
|
|
3493
|
+
"""
|
|
3494
|
+
nbus = c_int()
|
|
3495
|
+
iordvlt = c_int()
|
|
3496
|
+
e = (c_double * MAXBUS)()
|
|
3497
|
+
f = (c_double * MAXBUS)()
|
|
3498
|
+
base = (c_float * MAXBUS)()
|
|
3499
|
+
inp2opt = (c_int * MAXBUS)()
|
|
3500
|
+
opt2inp = (c_int * MAXBUS)()
|
|
3501
|
+
self._lib.ipfpy_voltage(e, f, base, inp2opt, opt2inp,
|
|
3502
|
+
byref(iordvlt), byref(nbus))
|
|
3503
|
+
n = nbus.value
|
|
3504
|
+
cbus = (c_char * (8 * MAXBUS))()
|
|
3505
|
+
czone = (c_char * (2 * MAXBUS))()
|
|
3506
|
+
cowner = (c_char * (3 * MAXBUS))()
|
|
3507
|
+
self._lib.ipfpy_busnames(byref(nbus), cbus, czone, cowner)
|
|
3508
|
+
names = [_text(bytes(cbus[i * 8:(i + 1) * 8])) for i in range(n)]
|
|
3509
|
+
zones = [_text(bytes(czone[i * 2:(i + 1) * 2])) for i in range(n)]
|
|
3510
|
+
owners = [_text(bytes(cowner[i * 3:(i + 1) * 3])) for i in range(n)]
|
|
3511
|
+
self._inp2opt = _arr(inp2opt, n, _np.int32 if _np else None)
|
|
3512
|
+
self._opt2inp = _arr(opt2inp, n, _np.int32 if _np else None)
|
|
3513
|
+
e_internal = _arr(e, n)
|
|
3514
|
+
f_internal = _arr(f, n)
|
|
3515
|
+
if iordvlt.value == 2:
|
|
3516
|
+
if _np is not None:
|
|
3517
|
+
input_to_opt = _np.asarray(
|
|
3518
|
+
self._inp2opt, dtype=_np.int64) - 1
|
|
3519
|
+
e_input = _np.asarray(e_internal)[input_to_opt].copy()
|
|
3520
|
+
f_input = _np.asarray(f_internal)[input_to_opt].copy()
|
|
3521
|
+
else:
|
|
3522
|
+
e_input = [e_internal[int(j) - 1] for j in self._inp2opt]
|
|
3523
|
+
f_input = [f_internal[int(j) - 1] for j in self._inp2opt]
|
|
3524
|
+
else:
|
|
3525
|
+
e_input, f_input = e_internal, f_internal
|
|
3526
|
+
return {
|
|
3527
|
+
"nbus": n,
|
|
3528
|
+
"ordvlt": iordvlt.value,
|
|
3529
|
+
"order": "input",
|
|
3530
|
+
"e": e_input,
|
|
3531
|
+
"f": f_input,
|
|
3532
|
+
"e_internal": e_internal,
|
|
3533
|
+
"f_internal": f_internal,
|
|
3534
|
+
"base": _arr(base, n, _np.float32 if _np else None),
|
|
3535
|
+
"names": names,
|
|
3536
|
+
"zone": zones,
|
|
3537
|
+
"owner": owners,
|
|
3538
|
+
"inp2opt": self._inp2opt,
|
|
3539
|
+
"opt2inp": self._opt2inp,
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
|
|
3543
|
+
def connect(url, token=None, timeout=60.0):
|
|
3544
|
+
"""Connect to a remote bpa-server (see :mod:`bpaflow.remote`)."""
|
|
3545
|
+
from .remote import connect as _connect
|
|
3546
|
+
return _connect(url, token=token, timeout=timeout)
|