dsh-plugin-pscad-expert-vyma 1.0.0
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.
- package/README.md +137 -0
- package/knowledge/pscad_skill_core.md +202 -0
- package/knowledge/templates_index.md +104 -0
- package/package.json +25 -0
- package/plugin.yaml +50 -0
- package/templates/fault_study.pscx +1171 -0
- package/templates/gfm_island_droop_nose.pscx +612 -0
- package/templates/grid_gfm_support.pscx +994 -0
- package/templates/island_droop_controller.pscx +1002 -0
- package/templates/line_model_pi_100km.pscx +306 -0
- package/templates/three_phase_cable_fault.pscx +1007 -0
- package/tools/__pycache__/main.cpython-37.pyc +0 -0
- package/tools/main.py +1195 -0
package/tools/main.py
ADDED
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""main.py -- PSCAD 自动化标准工具库(DSH 插件主入口,v1.2.0)
|
|
3
|
+
|
|
4
|
+
为 AI Agent(克隆体)提供操作 PSCAD(mhi.pscad 2.2.1 / PSCAD 5.0.0 Pro) 的
|
|
5
|
+
标准化执行工具。本库从多轮真实仿真测试脚本(L13~L19 认证轮、R4 科研轮)
|
|
6
|
+
中提取**验证成功**的代码逻辑重构而来。
|
|
7
|
+
|
|
8
|
+
工程规范:
|
|
9
|
+
1. **Agent 友好返回值**:所有公开函数以 try-except 包裹核心逻辑,绝不直接抛异常。
|
|
10
|
+
成功返回 ``{"status": "success", "data": {...}}``;
|
|
11
|
+
失败返回 ``{"status": "error", "message": "<原因 + traceback 摘要>"}``。
|
|
12
|
+
2. **完整 Type Hints 与 Docstring**:每个函数声明参数类型并说明用途/返回结构。
|
|
13
|
+
3. **动态配置持久化(v1.2.0)**:不再强依赖系统环境变量。路径配置保存在**工作区
|
|
14
|
+
本地文件 ``.pscad_config.json``**(默认当前工作目录),由 ``init_pscad_environment``
|
|
15
|
+
工具写入、``load_config`` 读取。环境变量 ``PSCAD_EXE_PATH``/``MHI_PYLIB_PATH``
|
|
16
|
+
仅作**降级回退**(未配置时自动回退,不崩溃)。
|
|
17
|
+
4. **依赖懒加载(v1.2.0)**:**模块顶部绝不 import mhi.pscad**。真正的
|
|
18
|
+
``import mhi.pscad`` 只在需要连接 PSCAD 的工具函数内部执行;导入前先
|
|
19
|
+
``load_config()`` 动态把 pylib 路径加入 ``sys.path``。若配置缺失,**不抛异常**,
|
|
20
|
+
返回标准 Agent 错误字典引导 AI:先调用 ``init_pscad_environment`` 向用户索要路径。
|
|
21
|
+
5. **状态持久化(v1.1.0)**:废弃跨进程失效的整数 ID 句柄。所有元件创建时被强制
|
|
22
|
+
赋予唯一 ``Name`` 标签;后续一切操作(连线/改参/桥接)通过遍历画布匹配
|
|
23
|
+
``Name`` 属性精准寻址——脚本重启/跨进程后依然有效。
|
|
24
|
+
|
|
25
|
+
关键经验(已固化为实现细节):
|
|
26
|
+
- PSCAD 启动必须带 ``/port:<port>`` 否则自动化服务不监听;崩溃后 Stop-Process 重启。
|
|
27
|
+
- 一切布线坐标取自 ``c.ports()``;wire 段经过任何非目标端口坐标会自动连接(短路)。
|
|
28
|
+
- datalabel 与端口连接须**同坐标(0 距离)**;同名 datalabel 无线互连(信号链标准做法)。
|
|
29
|
+
- source3 的 F 与 F0 必须成对设置(否则系统 60Hz 运行)。
|
|
30
|
+
- 工程同名重建会残留旧 build 消息——关键实验用唯一案例名。
|
|
31
|
+
|
|
32
|
+
首次使用引导::
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import main as pat
|
|
36
|
+
r = pat.init_pscad_environment(
|
|
37
|
+
exe_path=os.environ['PSCAD_EXE_PATH'], # PSCAD 可执行文件绝对路径
|
|
38
|
+
pylib_path=os.environ['MHI_PYLIB_PATH']) # mhi.pscad 库目录绝对路径
|
|
39
|
+
r = pat.connect_and_load_project('my_case', folder='./cases')
|
|
40
|
+
|
|
41
|
+
用法示例::
|
|
42
|
+
|
|
43
|
+
import main as pat
|
|
44
|
+
r = pat.connect_and_load_project('my_case', folder='./cases')
|
|
45
|
+
r = pat.add_and_configure_component('master:resistor', 600, 150,
|
|
46
|
+
params={'R': '100.0 [ohm]'}, name='R_new')
|
|
47
|
+
r = pat.connect_ports_safely('R_new', 'A', 'VrA_bus', 'B', bend='auto')
|
|
48
|
+
r = pat.configure_simulation(time_duration=0.5)
|
|
49
|
+
r = pat.run_simulation_and_monitor()
|
|
50
|
+
r = pat.read_output(channel='VrA', method='rms')
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import json
|
|
56
|
+
import os
|
|
57
|
+
import re
|
|
58
|
+
import sys
|
|
59
|
+
import time
|
|
60
|
+
import math
|
|
61
|
+
import subprocess
|
|
62
|
+
import traceback
|
|
63
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# 环境常量(端口可被环境变量覆盖;路径一律走本地配置,见 load_config)
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
CONFIG_FILE_NAME: str = '.pscad_config.json' # 工作区本地配置文件
|
|
69
|
+
PSCAD_PORT: int = int(os.environ.get("PSCAD_PORT", "54329"))
|
|
70
|
+
DEFAULT_FOLDER: str = os.environ.get("PSCAD_WORK_DIR", "") # 无硬编码;空则回退当前工作目录
|
|
71
|
+
LAUNCH_WAIT_S: int = 60
|
|
72
|
+
CONNECT_TIMEOUT_S: int = 20
|
|
73
|
+
|
|
74
|
+
MISSING_CONFIG_MSG = (
|
|
75
|
+
'未找到 PSCAD 路径配置。请先使用 init_pscad_environment 工具进行设置。'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 元件默认参数(实测合法值;调用方 params 覆盖默认值)
|
|
79
|
+
COMPONENT_DEFAULTS: Dict[str, Dict[str, Any]] = {
|
|
80
|
+
'master:source_1': {'Cntrl': 0, 'Grnd': 1, 'Vm': '132.8 [kV]',
|
|
81
|
+
'f': '50.0 [Hz]', 'R': '0.001 [ohm]', 'Ph': 0.0},
|
|
82
|
+
'master:source3': {'Vm': '230.0', 'F': '50.0 [Hz]', 'F0': '50.0 [Hz]',
|
|
83
|
+
'Es': '230.0', 'MVA': '1000.0', 'View': 'SINGLE_LINE',
|
|
84
|
+
'R1s': '0.1', 'L1p': '0.000001', 'Tc': '0.0'},
|
|
85
|
+
'master:newpi': {'len': '5.0E4', 'PU': 'R_XL_XC_OHM_', 'F': 50.0,
|
|
86
|
+
'Rp': '3.433E-5', 'Xp': '4.188E-4', 'Bp': '274.4'},
|
|
87
|
+
'master:resistor': {'R': '1.0 [ohm]'},
|
|
88
|
+
'master:inductor': {'L': '0.001 [H]'},
|
|
89
|
+
'master:breaker1': {'RON': '0.01', 'ROFF': '1.0E6'},
|
|
90
|
+
'master:tbreak': {'TC': '1.0 [s]', 'TO': '0.0 [s]', 'REP': 0},
|
|
91
|
+
'master:const': {'Value': 0.0, 'Dim': 1},
|
|
92
|
+
'master:realpole': {'T': 0.1, 'G': 1.0, 'YO': 0.0},
|
|
93
|
+
'master:sumjct': {'A': 'DISABLE', 'B': 'DISABLE', 'F': 'SUBTRACT',
|
|
94
|
+
'Dim': 1, 'DPath': 'REAL'},
|
|
95
|
+
'master:ground': {},
|
|
96
|
+
'master:ammeter': {},
|
|
97
|
+
'master:voltmetergnd': {},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Name 自动生成的类型提示前缀(仅影响可读性,不改变功能)
|
|
101
|
+
_NAME_HINT: Dict[str, str] = {
|
|
102
|
+
'resistor': 'R', 'inductor': 'L', 'source_1': 'S', 'source3': 'S3',
|
|
103
|
+
'ground': 'G', 'ammeter': 'AM', 'voltmetergnd': 'VM', 'const': 'K',
|
|
104
|
+
'mult': 'M', 'realpole': 'RP', 'sumjct': 'SJ', 'breaker1': 'BRK',
|
|
105
|
+
'tbreak': 'TB', 'datalabel': 'DL', 'pgb': 'PGB', 'newpi': 'PI',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# 本地配置持久化(工作区 .pscad_config.json)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
_config_cache: Optional[Dict[str, Any]] = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _config_path() -> str:
|
|
115
|
+
"""配置文件绝对路径(默认当前工作目录;可被环境变量 PSCAD_CONFIG 覆盖)。"""
|
|
116
|
+
base = os.environ.get('PSCAD_CONFIG_DIR') or os.getcwd()
|
|
117
|
+
return os.path.join(base, CONFIG_FILE_NAME)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_config() -> Dict[str, Any]:
|
|
121
|
+
"""加载工作区本地配置 ``.pscad_config.json``。
|
|
122
|
+
|
|
123
|
+
配置键:``pscad_exe_path``(PSCAD 可执行文件)、``mhi_pylib_path``
|
|
124
|
+
(mhi.pscad 库目录)、``pscad_port``(自动化端口,可选)、``saved_at``。
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
配置字典;文件不存在或损坏时返回 ``{}``(**绝不抛异常**)。
|
|
128
|
+
结果会缓存,供懒加载与连接逻辑读取。
|
|
129
|
+
"""
|
|
130
|
+
global _config_cache
|
|
131
|
+
if _config_cache is not None:
|
|
132
|
+
return _config_cache
|
|
133
|
+
try:
|
|
134
|
+
path = _config_path()
|
|
135
|
+
if os.path.isfile(path):
|
|
136
|
+
with open(path, encoding='utf-8') as fh:
|
|
137
|
+
_config_cache = json.load(fh)
|
|
138
|
+
else:
|
|
139
|
+
_config_cache = {}
|
|
140
|
+
except Exception:
|
|
141
|
+
_config_cache = {}
|
|
142
|
+
return _config_cache
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def save_config(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
146
|
+
"""将 PSCAD 路径配置持久化到工作区 ``.pscad_config.json``。
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
exe_path: PSCAD 可执行文件绝对路径(如 '<PSCAD_INSTALL_DIR>/bin/win64/Pscad.exe')。
|
|
150
|
+
pylib_path: mhi.pscad 库目录绝对路径(如 '<MHI_PYLIB_DIR>')。
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
success: data 含 ``config_path``、``pscad_exe_path``、``mhi_pylib_path``。
|
|
154
|
+
error: 写入失败(权限/路径异常)。
|
|
155
|
+
"""
|
|
156
|
+
global _config_cache
|
|
157
|
+
try:
|
|
158
|
+
cfg = {
|
|
159
|
+
'pscad_exe_path': str(exe_path),
|
|
160
|
+
'mhi_pylib_path': str(pylib_path),
|
|
161
|
+
'pscad_port': PSCAD_PORT,
|
|
162
|
+
'saved_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
163
|
+
}
|
|
164
|
+
path = _config_path()
|
|
165
|
+
with open(path, 'w', encoding='utf-8') as fh:
|
|
166
|
+
json.dump(cfg, fh, ensure_ascii=False, indent=2)
|
|
167
|
+
_config_cache = cfg
|
|
168
|
+
return _ok({'config_path': path, 'pscad_exe_path': cfg['pscad_exe_path'],
|
|
169
|
+
'mhi_pylib_path': cfg['mhi_pylib_path'],
|
|
170
|
+
'pscad_port': cfg['pscad_port']})
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
return _err(exc)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def init_pscad_environment(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
176
|
+
"""**初始化工具**:配置 PSCAD 运行环境并持久化(AI 首个应调用的工具)。
|
|
177
|
+
|
|
178
|
+
接收用户提供的 PSCAD 可执行文件路径与 mhi.pscad 库目录路径,写入工作区
|
|
179
|
+
``.pscad_config.json``。配置持久化后,连接/启动类工具(connect_and_load_project、
|
|
180
|
+
restart_pscad 等)将自动懒加载该配置,无需任何系统环境变量。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
exe_path: PSCAD 可执行文件绝对路径。
|
|
184
|
+
pylib_path: mhi.pscad 库目录绝对路径。
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
success: data 含 ``config_path``、``pscad_exe_path``、``mhi_pylib_path``
|
|
188
|
+
(与 save_config 一致,另附 ``message`` 引导语)。
|
|
189
|
+
error: 写入失败。
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
r = save_config(exe_path, pylib_path)
|
|
193
|
+
if r['status'] == 'success':
|
|
194
|
+
r['data']['message'] = ('PSCAD 环境配置已保存。后续连接将自动使用该配置,'
|
|
195
|
+
'可开始调用 connect_and_load_project 加载工程。')
|
|
196
|
+
return r
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
return _err(exc)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _cfg_exe() -> Optional[str]:
|
|
202
|
+
"""解析 PSCAD 可执行文件路径:本地配置优先,环境变量降级回退。"""
|
|
203
|
+
cfg = load_config()
|
|
204
|
+
exe = cfg.get('pscad_exe_path')
|
|
205
|
+
if not exe:
|
|
206
|
+
exe = os.environ.get('PSCAD_EXE_PATH')
|
|
207
|
+
return exe or None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cfg_pylib() -> Optional[str]:
|
|
211
|
+
"""解析 mhi.pscad 库目录:本地配置优先,环境变量降级回退。"""
|
|
212
|
+
cfg = load_config()
|
|
213
|
+
pylib = cfg.get('mhi_pylib_path')
|
|
214
|
+
if not pylib:
|
|
215
|
+
pylib = os.environ.get('MHI_PYLIB_PATH')
|
|
216
|
+
return pylib or None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# 模块级会话状态(单例;组件寻址一律走 Name,无内存注册表)
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
_pscad: Any = None # mhi.pscad.PSCAD 连接(懒加载后赋值)
|
|
223
|
+
_prj: Any = None # 当前工程对象
|
|
224
|
+
_prj_name: str = '' # 当前工程名(唯一案例名)
|
|
225
|
+
_prj_folder: str = DEFAULT_FOLDER
|
|
226
|
+
_main: Any = None # 当前画布(Main)
|
|
227
|
+
_name_seq: List[int] = [1] # Name 自动生成计数器
|
|
228
|
+
_pgb_slot: List[int] = [0] # pgb 自动布点计数器
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _ok(data: Any) -> Dict[str, Any]:
|
|
232
|
+
"""构造成功返回。"""
|
|
233
|
+
return {"status": "success", "data": data}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _err(exc: Union[Exception, str]) -> Dict[str, Any]:
|
|
237
|
+
"""构造失败返回:捕获 traceback 摘要,绝不抛出。"""
|
|
238
|
+
if isinstance(exc, Exception):
|
|
239
|
+
tb = traceback.format_exc(limit=6)
|
|
240
|
+
msg = '%s: %s' % (type(exc).__name__, exc)
|
|
241
|
+
return {"status": "error", "message": msg, "trace": tb}
|
|
242
|
+
return {"status": "error", "message": str(exc)}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _import_mhi() -> Tuple[bool, str]:
|
|
246
|
+
"""懒加载 mhi.pscad(仅在实际需要连接 PSCAD 时调用)。
|
|
247
|
+
|
|
248
|
+
流程:``load_config()`` 取 pylib 路径 → 动态加入 ``sys.path`` → import。
|
|
249
|
+
**配置缺失时不抛异常**,返回引导消息:先调用 ``init_pscad_environment``。
|
|
250
|
+
"""
|
|
251
|
+
pylib = _cfg_pylib()
|
|
252
|
+
if not pylib:
|
|
253
|
+
return False, MISSING_CONFIG_MSG
|
|
254
|
+
try:
|
|
255
|
+
if 'mhi.pscad' not in sys.modules:
|
|
256
|
+
if pylib not in sys.path:
|
|
257
|
+
sys.path.insert(0, pylib)
|
|
258
|
+
import mhi.pscad # noqa: F401
|
|
259
|
+
return True, ''
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
return False, ('import mhi.pscad 失败(mhi_pylib_path=%s): %s'
|
|
262
|
+
% (pylib, exc))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# 组件寻址:遍历画布按 Name 属性匹配(跨进程持久)
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
def _name_of(component: Any) -> str:
|
|
269
|
+
"""读取组件 Name 参数(不同元件可能返回空字符串)。"""
|
|
270
|
+
try:
|
|
271
|
+
p = component.parameters()
|
|
272
|
+
if isinstance(p, dict):
|
|
273
|
+
v = p.get('Name')
|
|
274
|
+
if v is None:
|
|
275
|
+
v = p.get('name')
|
|
276
|
+
return '' if v is None else str(v)
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
return ''
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _iter_components() -> List[Any]:
|
|
283
|
+
"""尽力遍历当前画布上的全部组件(多级 fallback,失败返回空列表)。"""
|
|
284
|
+
if _main is None:
|
|
285
|
+
return []
|
|
286
|
+
try:
|
|
287
|
+
items = _main.find_all()
|
|
288
|
+
if items:
|
|
289
|
+
return list(items)
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
try:
|
|
293
|
+
items = _main.find_all(Name='*')
|
|
294
|
+
if items:
|
|
295
|
+
return list(items)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _find_by_name(name: str) -> Optional[Any]:
|
|
302
|
+
"""按 Name 属性精准寻址组件(遍历画布匹配)。
|
|
303
|
+
|
|
304
|
+
查找顺序:find_first 精确命中(校验 Name 参数)→ find_all 过滤 →
|
|
305
|
+
全画布遍历比对 Name 参数。找不到返回 None。
|
|
306
|
+
"""
|
|
307
|
+
if _main is None or not name:
|
|
308
|
+
return None
|
|
309
|
+
try:
|
|
310
|
+
c = _main.find_first(name)
|
|
311
|
+
if c is not None and _name_of(c) == name:
|
|
312
|
+
return c
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
try:
|
|
316
|
+
items = _main.find_all(Name=name)
|
|
317
|
+
if items:
|
|
318
|
+
for c in items:
|
|
319
|
+
if _name_of(c) == name:
|
|
320
|
+
return c
|
|
321
|
+
return items[0]
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
for c in _iter_components():
|
|
325
|
+
try:
|
|
326
|
+
if _name_of(c) == name:
|
|
327
|
+
return c
|
|
328
|
+
except Exception:
|
|
329
|
+
continue
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _collect_all_ports(exclude_names: Optional[set] = None) -> List[Tuple[int, int]]:
|
|
334
|
+
"""收集画布上全部组件的端口坐标(供穿线检测;exclude_names 内组件除外)。"""
|
|
335
|
+
ports: List[Tuple[int, int]] = []
|
|
336
|
+
exclude_names = exclude_names or set()
|
|
337
|
+
for c in _iter_components():
|
|
338
|
+
try:
|
|
339
|
+
if _name_of(c) in exclude_names:
|
|
340
|
+
continue
|
|
341
|
+
for _k, v in c.ports().items():
|
|
342
|
+
ports.append(tuple(v[:2]))
|
|
343
|
+
except Exception:
|
|
344
|
+
continue
|
|
345
|
+
return ports
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _gen_name(comp_type: str) -> str:
|
|
349
|
+
"""生成唯一 Name 标签(如 R_1、AM_2);显式 name 优先。"""
|
|
350
|
+
short = comp_type.split(':')[-1]
|
|
351
|
+
hint = _NAME_HINT.get(short, short[:2].upper())
|
|
352
|
+
n = _name_seq[0]
|
|
353
|
+
_name_seq[0] += 1
|
|
354
|
+
return '%s_%d' % (hint, n)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# 工具集 1:连接与工程加载(懒加载 mhi)
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
def connect_and_load_project(project_name: str,
|
|
361
|
+
folder: Optional[str] = None,
|
|
362
|
+
create_if_missing: bool = False,
|
|
363
|
+
auto_launch: bool = True) -> Dict[str, Any]:
|
|
364
|
+
"""启动/连接 PSCAD 实例,并加载(或新建)指定的 .pscx 模板工程。
|
|
365
|
+
|
|
366
|
+
首次调用会自动 ``load_config()`` 并懒加载 mhi.pscad;若未配置路径,
|
|
367
|
+
返回引导错误(不崩溃),请先调用 ``init_pscad_environment``。
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
project_name: 工程名(不带 .pscx 后缀);将作为唯一案例名。
|
|
371
|
+
folder: 工程所在目录;None 时用 PSCAD_WORK_DIR 环境变量(默认值兜底)。
|
|
372
|
+
create_if_missing: 工程不存在时是否用 ``create_case`` 新建空工程。
|
|
373
|
+
auto_launch: 连接失败时是否自动拉起 PSCAD 进程。
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
success: data 含 ``project``、``canvas``('Main')。
|
|
377
|
+
error: 未配置路径(引导消息)/连接/加载失败原因。
|
|
378
|
+
"""
|
|
379
|
+
global _pscad, _prj, _prj_name, _prj_folder, _main
|
|
380
|
+
try:
|
|
381
|
+
ok, msg = _ensure_connected()
|
|
382
|
+
if not ok and auto_launch:
|
|
383
|
+
launched, msg2 = _launch_pscad()
|
|
384
|
+
if not launched:
|
|
385
|
+
return _err('%s;自动启动失败: %s' % (msg, msg2))
|
|
386
|
+
ok, msg = _ensure_connected()
|
|
387
|
+
if not ok:
|
|
388
|
+
return _err(msg)
|
|
389
|
+
|
|
390
|
+
folder = folder or (DEFAULT_FOLDER or os.getcwd())
|
|
391
|
+
_prj_folder = folder
|
|
392
|
+
|
|
393
|
+
prj = None
|
|
394
|
+
try:
|
|
395
|
+
prj = _pscad.project(project_name)
|
|
396
|
+
except Exception:
|
|
397
|
+
prj = None
|
|
398
|
+
|
|
399
|
+
if prj is None:
|
|
400
|
+
pscx = os.path.join(folder, project_name + '.pscx')
|
|
401
|
+
if os.path.isfile(pscx):
|
|
402
|
+
try:
|
|
403
|
+
_pscad.load(pscx.replace('\\', '/'))
|
|
404
|
+
prj = _pscad.project(project_name)
|
|
405
|
+
except Exception as exc:
|
|
406
|
+
return _err('加载 %s 失败: %s' % (pscx, exc))
|
|
407
|
+
|
|
408
|
+
if prj is None and create_if_missing:
|
|
409
|
+
try:
|
|
410
|
+
_pscad.create_case(project_name, folder=folder)
|
|
411
|
+
prj = _pscad.project(project_name)
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
return _err('create_case 失败: %s' % exc)
|
|
414
|
+
|
|
415
|
+
if prj is None:
|
|
416
|
+
return _err('工程 "%s" 未找到(folder=%s,未创建)'
|
|
417
|
+
% (project_name, folder))
|
|
418
|
+
|
|
419
|
+
_prj = prj
|
|
420
|
+
_prj_name = project_name
|
|
421
|
+
try:
|
|
422
|
+
_main = prj.canvas('Main')
|
|
423
|
+
except Exception as exc:
|
|
424
|
+
_main = None
|
|
425
|
+
return _err('打开 Main 画布失败: %s' % exc)
|
|
426
|
+
return _ok({'project': project_name, 'canvas': 'Main'})
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
return _err(exc)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _ensure_connected() -> Tuple[bool, str]:
|
|
432
|
+
"""确保存在活的 mhi 连接(幂等;懒加载 mhi)。"""
|
|
433
|
+
global _pscad
|
|
434
|
+
ok, msg = _import_mhi()
|
|
435
|
+
if not ok:
|
|
436
|
+
return False, msg
|
|
437
|
+
if _pscad is not None:
|
|
438
|
+
try:
|
|
439
|
+
if _pscad.is_alive():
|
|
440
|
+
return True, ''
|
|
441
|
+
except Exception:
|
|
442
|
+
_pscad = None
|
|
443
|
+
try:
|
|
444
|
+
_pscad = mhi.pscad.connect(port=PSCAD_PORT, timeout=CONNECT_TIMEOUT_S)
|
|
445
|
+
return True, ''
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
return False, '连接 PSCAD 失败(port=%d): %s' % (PSCAD_PORT, exc)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _launch_pscad() -> Tuple[bool, str]:
|
|
451
|
+
"""拉起 PSCAD 进程并等待自动化端口就绪(崩溃恢复标准路径;懒加载 exe 路径)。"""
|
|
452
|
+
global _pscad
|
|
453
|
+
exe = _cfg_exe()
|
|
454
|
+
if not exe:
|
|
455
|
+
return False, MISSING_CONFIG_MSG
|
|
456
|
+
try:
|
|
457
|
+
subprocess.Popen([exe, '/startup:au',
|
|
458
|
+
'/port:%d' % PSCAD_PORT, '/splash:false'])
|
|
459
|
+
deadline = time.time() + LAUNCH_WAIT_S
|
|
460
|
+
while time.time() < deadline:
|
|
461
|
+
time.sleep(2)
|
|
462
|
+
try:
|
|
463
|
+
_pscad = mhi.pscad.connect(port=PSCAD_PORT, timeout=5)
|
|
464
|
+
return True, ''
|
|
465
|
+
except Exception:
|
|
466
|
+
continue
|
|
467
|
+
return False, 'PSCAD 启动后 %ds 内端口未就绪' % LAUNCH_WAIT_S
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
return False, str(exc)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def restart_pscad() -> Dict[str, Any]:
|
|
473
|
+
"""强制重启 PSCAD(自动化服务卡死/10054 崩溃后的恢复)。
|
|
474
|
+
|
|
475
|
+
终止现有 PSCAD 进程、重新启动并重连,然后重新加载当前工程。
|
|
476
|
+
"""
|
|
477
|
+
global _pscad, _prj, _main
|
|
478
|
+
try:
|
|
479
|
+
if _pscad is not None:
|
|
480
|
+
try:
|
|
481
|
+
_pscad.close_connection()
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
_pscad = None
|
|
485
|
+
subprocess.run(['taskkill', '/F', '/IM', 'Pscad.exe'],
|
|
486
|
+
capture_output=True, timeout=30)
|
|
487
|
+
time.sleep(5)
|
|
488
|
+
ok, msg = _launch_pscad()
|
|
489
|
+
if not ok:
|
|
490
|
+
return _err(msg)
|
|
491
|
+
if _prj_name:
|
|
492
|
+
return connect_and_load_project(_prj_name, folder=_prj_folder)
|
|
493
|
+
return _ok({'restarted': True, 'project': None})
|
|
494
|
+
except Exception as exc:
|
|
495
|
+
return _err(exc)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def close_connection() -> Dict[str, Any]:
|
|
499
|
+
"""礼貌关闭自动化连接(脚本收尾时调用,避免占用端口)。"""
|
|
500
|
+
global _pscad, _prj, _main
|
|
501
|
+
try:
|
|
502
|
+
if _pscad is not None:
|
|
503
|
+
try:
|
|
504
|
+
_pscad.close_connection()
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
_pscad = None
|
|
508
|
+
_prj = None
|
|
509
|
+
_main = None
|
|
510
|
+
return _ok({'closed': True})
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
return _err(exc)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# ---------------------------------------------------------------------------
|
|
516
|
+
# 工具集 2:组件放置与配置(Name 标签寻址)
|
|
517
|
+
# ---------------------------------------------------------------------------
|
|
518
|
+
def _require_canvas() -> Optional[Dict[str, Any]]:
|
|
519
|
+
"""前置检查:已加载工程且 Main 画布可用。失败返回 error dict,否则 None。"""
|
|
520
|
+
if _main is None:
|
|
521
|
+
return _err('尚未加载工程画布,请先调用 connect_and_load_project()')
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def add_and_configure_component(comp_type: str, x: int, y: int,
|
|
526
|
+
params: Optional[Dict[str, Any]] = None,
|
|
527
|
+
orient: int = 0,
|
|
528
|
+
name: Optional[str] = None,
|
|
529
|
+
use_defaults: bool = True) -> Dict[str, Any]:
|
|
530
|
+
"""在当前画布指定坐标放置元件并应用参数字典;**强制赋予唯一 Name 标签**。
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
comp_type: 元件定义名,如 'master:resistor'、'resistor'(自动补 master:)、
|
|
534
|
+
自定义组件 '工程名:DEFN'(须先 create_definition)。
|
|
535
|
+
x, y: 放置锚点坐标(mhi 网格坐标)。
|
|
536
|
+
params: 参数键值对(带单位字符串如 '100.0 [ohm]')。
|
|
537
|
+
orient: 旋转取模 4(0=0° 1=90° 2=180° 3=270°)。
|
|
538
|
+
name: 显式 Name 标签;None 时自动生成唯一 Name('R_1' 风格)。
|
|
539
|
+
**注意**:ammeter/voltmetergnd 的 Name 即输出通道名;datalabel 的
|
|
540
|
+
Name 即信号名——显式命名可同时服务于寻址与信号语义。
|
|
541
|
+
use_defaults: 是否合并 COMPONENT_DEFAULTS 中的已验证默认参数。
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
success: data 含 ``name``(唯一 Name 标签,后续寻址主键)、``component``、
|
|
545
|
+
``defn``、``ports``({端口名: (x, y)})、``position``、``orient``。
|
|
546
|
+
error: 创建/配置失败。
|
|
547
|
+
"""
|
|
548
|
+
try:
|
|
549
|
+
bad = _require_canvas()
|
|
550
|
+
if bad:
|
|
551
|
+
return bad
|
|
552
|
+
full = comp_type if ':' in comp_type else 'master:' + comp_type
|
|
553
|
+
final_name = name if name else _gen_name(full)
|
|
554
|
+
kw = dict(params or {})
|
|
555
|
+
kw['Name'] = final_name
|
|
556
|
+
if use_defaults and full in COMPONENT_DEFAULTS:
|
|
557
|
+
merged = dict(COMPONENT_DEFAULTS[full])
|
|
558
|
+
merged.update(kw)
|
|
559
|
+
kw = merged
|
|
560
|
+
comp = _main.create_component(full, x, y, orient=orient, **kw)
|
|
561
|
+
ports: Dict[str, Tuple[int, int]] = {}
|
|
562
|
+
try:
|
|
563
|
+
ports = {k: tuple(v[:2]) for k, v in comp.ports().items()}
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
return _ok({'name': final_name, 'component': comp, 'defn': full,
|
|
567
|
+
'ports': ports, 'position': (x, y), 'orient': orient})
|
|
568
|
+
except Exception as exc:
|
|
569
|
+
return _err(exc)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def get_component(name: str) -> Dict[str, Any]:
|
|
573
|
+
"""按 Name 标签寻址组件(遍历画布匹配,跨进程持久有效)。
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
success: data 含 ``component``、``ports``、``name``。
|
|
577
|
+
error: 未找到。
|
|
578
|
+
"""
|
|
579
|
+
try:
|
|
580
|
+
comp = _find_by_name(name)
|
|
581
|
+
if comp is None:
|
|
582
|
+
return _err('未找到 Name="%s" 的组件(当前画布已加载:%s)'
|
|
583
|
+
% (name, _prj_name or '?'))
|
|
584
|
+
ports = {}
|
|
585
|
+
try:
|
|
586
|
+
ports = {k: tuple(v[:2]) for k, v in comp.ports().items()}
|
|
587
|
+
except Exception:
|
|
588
|
+
pass
|
|
589
|
+
return _ok({'component': comp, 'ports': ports, 'name': name})
|
|
590
|
+
except Exception as exc:
|
|
591
|
+
return _err(exc)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def list_components() -> Dict[str, Any]:
|
|
595
|
+
"""列出当前画布全部组件的 Name 标签与端口(遍历画布,寻址调试用)。
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
success: data 含 ``components``([{name, ports}] 列表) 与 ``count``。
|
|
599
|
+
"""
|
|
600
|
+
try:
|
|
601
|
+
items = []
|
|
602
|
+
for c in _iter_components():
|
|
603
|
+
nm = _name_of(c)
|
|
604
|
+
ports = {}
|
|
605
|
+
try:
|
|
606
|
+
ports = {k: tuple(v[:2]) for k, v in c.ports().items()}
|
|
607
|
+
except Exception:
|
|
608
|
+
pass
|
|
609
|
+
items.append({'name': nm, 'ports': ports})
|
|
610
|
+
return _ok({'components': items, 'count': len(items)})
|
|
611
|
+
except Exception as exc:
|
|
612
|
+
return _err(exc)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def set_component_params(name: str,
|
|
616
|
+
params: Dict[str, Any]) -> Dict[str, Any]:
|
|
617
|
+
"""就地修改已放置组件的参数(如负荷电阻、下垂系数、tbreak 时序)。
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
name: 目标组件 Name 标签。
|
|
621
|
+
params: 参数键值对。
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
success: data 含 ``applied``(实际生效参数)。
|
|
625
|
+
"""
|
|
626
|
+
try:
|
|
627
|
+
r = get_component(name)
|
|
628
|
+
if r['status'] != 'success':
|
|
629
|
+
return r
|
|
630
|
+
comp = r['data']['component']
|
|
631
|
+
comp.parameters(parameters=dict(params))
|
|
632
|
+
return _ok({'applied': dict(params)})
|
|
633
|
+
except Exception as exc:
|
|
634
|
+
return _err(exc)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# ---------------------------------------------------------------------------
|
|
638
|
+
# 工具集 3:安全连线(拓扑黄金法则实现,Name 寻址)
|
|
639
|
+
# ---------------------------------------------------------------------------
|
|
640
|
+
def _seg_hits(p1: Tuple[int, int], p2: Tuple[int, int],
|
|
641
|
+
ports: List[Tuple[int, int]]) -> bool:
|
|
642
|
+
"""检查水平/垂直线段是否经过 ports 中的任何坐标(端点本身除外)。"""
|
|
643
|
+
x1, y1 = p1
|
|
644
|
+
x2, y2 = p2
|
|
645
|
+
for px, py in ports:
|
|
646
|
+
if (px, py) == p1 or (px, py) == p2:
|
|
647
|
+
continue
|
|
648
|
+
if y1 == y2 and py == y1 and min(x1, x2) <= px <= max(x1, x2):
|
|
649
|
+
return True
|
|
650
|
+
if x1 == x2 and px == x1 and min(y1, y2) <= py <= max(y1, y2):
|
|
651
|
+
return True
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _route(p1: Tuple[int, int], p2: Tuple[int, int],
|
|
656
|
+
ports: List[Tuple[int, int]], bend: str,
|
|
657
|
+
clearance: int) -> Tuple[List[Tuple[int, int]], List[str]]:
|
|
658
|
+
"""生成避开端口的安全正交路径(直连/L 型/U 型绕行)。
|
|
659
|
+
|
|
660
|
+
返回 (路径点列表, 警告列表);无安全路径时返回第一条候选并附警告。
|
|
661
|
+
"""
|
|
662
|
+
warnings: List[str] = []
|
|
663
|
+
x1, y1 = p1
|
|
664
|
+
x2, y2 = p2
|
|
665
|
+
candidates: List[List[Tuple[int, int]]] = []
|
|
666
|
+
if x1 == x2 or y1 == y2:
|
|
667
|
+
candidates.append([p1, p2])
|
|
668
|
+
else:
|
|
669
|
+
if bend in ('auto', 'hfirst'):
|
|
670
|
+
candidates.append([p1, (x2, y1), p2])
|
|
671
|
+
if bend in ('auto', 'vfirst'):
|
|
672
|
+
candidates.append([p1, (x1, y2), p2])
|
|
673
|
+
if bend in ('auto', 'top', 'bottom'):
|
|
674
|
+
dy = -clearance if bend == 'top' else clearance
|
|
675
|
+
candidates.append([p1, (x1, y1 + dy), (x2, y1 + dy), p2])
|
|
676
|
+
candidates.append([p1, (x1, y1 - clearance), (x2, y1 - clearance), p2])
|
|
677
|
+
for path in candidates:
|
|
678
|
+
clean = True
|
|
679
|
+
for a, b in zip(path, path[1:]):
|
|
680
|
+
if _seg_hits(a, b, ports):
|
|
681
|
+
clean = False
|
|
682
|
+
break
|
|
683
|
+
if clean:
|
|
684
|
+
return path, warnings
|
|
685
|
+
warnings.append('未找到完全避让端口的路径,采用候选路径(可能有穿线风险,'
|
|
686
|
+
'请检查 build messages 或运行 check_component_overlaps)')
|
|
687
|
+
return candidates[0], warnings
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def connect_ports_safely(source_name: str, source_port: str,
|
|
691
|
+
target_name: str, target_port: str,
|
|
692
|
+
bend: str = 'auto',
|
|
693
|
+
clearance: int = 20) -> Dict[str, Any]:
|
|
694
|
+
"""精准连接两个组件的指定端口(按 Name 寻址;防悬空/穿线短路)。
|
|
695
|
+
|
|
696
|
+
内部逻辑(已验证的黄金法则):
|
|
697
|
+
1. 按 Name 遍历画布寻址两端组件,从 ``ports()`` 动态取绝对坐标;
|
|
698
|
+
2. 校验端口名存在性;
|
|
699
|
+
3. 枚举画布其余组件端口,生成避让的正交折线(L 型/U 型绕行);
|
|
700
|
+
4. ``create_wire`` 落线——wire 段不经过任何非目标端口坐标。
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
source_name: 源组件 Name 标签。
|
|
704
|
+
source_port: 源端口名(如 resistor 的 'A'、ammeter 的 'N2')。
|
|
705
|
+
target_name: 目标组件 Name 标签。
|
|
706
|
+
target_port: 目标端口名。
|
|
707
|
+
bend: 'auto'(默认) / 'hfirst' / 'vfirst' / 'top' / 'bottom'。
|
|
708
|
+
clearance: U 型绕行的垂直间距(网格单位,默认 20)。
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
success: data 含 ``wire_points``、``source_coord``、``target_coord``、
|
|
712
|
+
``routed``、``warnings``。
|
|
713
|
+
error: 端口不存在/组件未找到/坐标重叠等。
|
|
714
|
+
"""
|
|
715
|
+
try:
|
|
716
|
+
bad = _require_canvas()
|
|
717
|
+
if bad:
|
|
718
|
+
return bad
|
|
719
|
+
r1 = get_component(source_name)
|
|
720
|
+
r2 = get_component(target_name)
|
|
721
|
+
if r1['status'] != 'success':
|
|
722
|
+
return r1
|
|
723
|
+
if r2['status'] != 'success':
|
|
724
|
+
return r2
|
|
725
|
+
comp1 = r1['data']['component']
|
|
726
|
+
comp2 = r2['data']['component']
|
|
727
|
+
ps1 = comp1.ports()
|
|
728
|
+
ps2 = comp2.ports()
|
|
729
|
+
if source_port not in ps1:
|
|
730
|
+
return _err('端口 "%s" 不存在于组件 "%s",可用: %s'
|
|
731
|
+
% (source_port, source_name, sorted(ps1)))
|
|
732
|
+
if target_port not in ps2:
|
|
733
|
+
return _err('端口 "%s" 不存在于组件 "%s",可用: %s'
|
|
734
|
+
% (target_port, target_name, sorted(ps2)))
|
|
735
|
+
p1 = tuple(ps1[source_port][:2])
|
|
736
|
+
p2 = tuple(ps2[target_port][:2])
|
|
737
|
+
if p1 == p2:
|
|
738
|
+
return _err('源/目标端口坐标相同 %s——组件锚点重叠,请调整布局' % (p1,))
|
|
739
|
+
other_ports = _collect_all_ports(exclude_names={source_name, target_name})
|
|
740
|
+
path, warnings = _route(p1, p2, other_ports, bend, clearance)
|
|
741
|
+
_main.create_wire(*path)
|
|
742
|
+
return _ok({'wire_points': path, 'source_coord': p1,
|
|
743
|
+
'target_coord': p2, 'routed': len(warnings) == 0,
|
|
744
|
+
'warnings': warnings})
|
|
745
|
+
except Exception as exc:
|
|
746
|
+
return _err(exc)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def connect_ports_chain(specs: List[Tuple[str, str, str, str]],
|
|
750
|
+
bend: str = 'auto') -> Dict[str, Any]:
|
|
751
|
+
"""批量链式连线:[(src_name, src_port, tgt_name, tgt_port), ...] 顺序执行。
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
success: data 含 ``results``(每条 connect_ports_safely 的返回值)。
|
|
755
|
+
"""
|
|
756
|
+
try:
|
|
757
|
+
results = []
|
|
758
|
+
for spec in specs:
|
|
759
|
+
results.append(connect_ports_safely(spec[0], spec[1],
|
|
760
|
+
spec[2], spec[3], bend=bend))
|
|
761
|
+
return _ok({'results': results})
|
|
762
|
+
except Exception as exc:
|
|
763
|
+
return _err(exc)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def connect_signal_by_name(source_name: str, source_port: str,
|
|
767
|
+
signal_name: str,
|
|
768
|
+
target_name: str, target_port: str) -> Dict[str, Any]:
|
|
769
|
+
"""用同名 datalabel 桥接数据/控制信号(信号链标准做法,无需 wire)。
|
|
770
|
+
|
|
771
|
+
机制(实测):datalabel 端口 = 锚点;与组件端口**同坐标(0 距离)**放置即
|
|
772
|
+
吸附连接并重命名该端口信号;两个同名 datalabel 无线互连。适用于 const→
|
|
773
|
+
mult 输入、const→source Mag/freq 端口、控制器输出→端口等全部数据信号。
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
source_name: 源组件 Name(信号产生端,如 const)。
|
|
777
|
+
source_port: 源端口名(如 const 的 'OUT')。
|
|
778
|
+
signal_name: 信号名(须唯一;多个同名即广播)。
|
|
779
|
+
target_name: 目标组件 Name(信号消费端)。
|
|
780
|
+
target_port: 目标端口名(如 source_1 的 'Mag' / 'freq')。
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
success: data 含 ``signal``、两端坐标、两端 datalabel 放置点。
|
|
784
|
+
error: 组件/端口不存在等。
|
|
785
|
+
"""
|
|
786
|
+
try:
|
|
787
|
+
bad = _require_canvas()
|
|
788
|
+
if bad:
|
|
789
|
+
return bad
|
|
790
|
+
r1 = get_component(source_name)
|
|
791
|
+
r2 = get_component(target_name)
|
|
792
|
+
if r1['status'] != 'success':
|
|
793
|
+
return r1
|
|
794
|
+
if r2['status'] != 'success':
|
|
795
|
+
return r2
|
|
796
|
+
ps1 = r1['data']['component'].ports()
|
|
797
|
+
ps2 = r2['data']['component'].ports()
|
|
798
|
+
if source_port not in ps1:
|
|
799
|
+
return _err('端口 "%s" 不存在于组件 "%s"' % (source_port, source_name))
|
|
800
|
+
if target_port not in ps2:
|
|
801
|
+
return _err('端口 "%s" 不存在于组件 "%s"' % (target_port, target_name))
|
|
802
|
+
p1 = tuple(ps1[source_port][:2])
|
|
803
|
+
p2 = tuple(ps2[target_port][:2])
|
|
804
|
+
if p1 == p2:
|
|
805
|
+
return _err('源/目标端口坐标相同 %s——组件锚点重叠' % (p1,))
|
|
806
|
+
_main.create_component('master:datalabel', p1[0], p1[1], orient=0,
|
|
807
|
+
Name=signal_name)
|
|
808
|
+
_main.create_component('master:datalabel', p2[0], p2[1], orient=0,
|
|
809
|
+
Name=signal_name)
|
|
810
|
+
return _ok({'signal': signal_name, 'source_coord': p1,
|
|
811
|
+
'target_coord': p2})
|
|
812
|
+
except Exception as exc:
|
|
813
|
+
return _err(exc)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def register_pgb(channel_name: str, x: Optional[int] = None,
|
|
817
|
+
y: Optional[int] = None) -> Dict[str, Any]:
|
|
818
|
+
"""注册 PGB 输出通道(pgb + 同名 datalabel)。
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
channel_name: 输出通道名(与目标信号同名即记录该信号)。
|
|
822
|
+
x, y: pgb 锚点;None 时自动布点(x=900 起,每 40 递增)。
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
success: data 含 ``channel``、``position``。
|
|
826
|
+
"""
|
|
827
|
+
try:
|
|
828
|
+
bad = _require_canvas()
|
|
829
|
+
if bad:
|
|
830
|
+
return bad
|
|
831
|
+
if x is None or y is None:
|
|
832
|
+
slot = _pgb_slot[0]
|
|
833
|
+
_pgb_slot[0] += 1
|
|
834
|
+
x = 900 + (slot % 4) * 60
|
|
835
|
+
y = 100 + (slot // 4) * 40
|
|
836
|
+
_main.create_component('master:pgb', x, y, orient=0, Name=channel_name)
|
|
837
|
+
_main.create_component('master:datalabel', x, y, orient=0,
|
|
838
|
+
Name=channel_name)
|
|
839
|
+
return _ok({'channel': channel_name, 'position': (x, y)})
|
|
840
|
+
except Exception as exc:
|
|
841
|
+
return _err(exc)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def check_component_overlaps(min_gap: int = 2) -> Dict[str, Any]:
|
|
845
|
+
"""诊断:遍历画布全部组件,检查锚点/端口重叠(build 前自检)。
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
success: data 含 ``overlaps``(重叠对列表,空列表=安全)与 ``checked``。
|
|
849
|
+
"""
|
|
850
|
+
try:
|
|
851
|
+
items = _iter_components()
|
|
852
|
+
overlaps = []
|
|
853
|
+
for i in range(len(items)):
|
|
854
|
+
for j in range(i + 1, len(items)):
|
|
855
|
+
c1, c2 = items[i], items[j]
|
|
856
|
+
try:
|
|
857
|
+
ps1 = [tuple(v[:2]) for v in c1.ports().values()]
|
|
858
|
+
ps2 = [tuple(v[:2]) for v in c2.ports().values()]
|
|
859
|
+
except Exception:
|
|
860
|
+
continue
|
|
861
|
+
for a in ps1:
|
|
862
|
+
for b in ps2:
|
|
863
|
+
if abs(a[0] - b[0]) <= min_gap and abs(a[1] - b[1]) <= min_gap:
|
|
864
|
+
overlaps.append({'name': _name_of(c1), 'port': a,
|
|
865
|
+
'vs': _name_of(c2), 'port': b})
|
|
866
|
+
return _ok({'overlaps': overlaps, 'checked': len(items)})
|
|
867
|
+
except Exception as exc:
|
|
868
|
+
return _err(exc)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
# ---------------------------------------------------------------------------
|
|
872
|
+
# 工具集 4:仿真配置、运行与监控
|
|
873
|
+
# ---------------------------------------------------------------------------
|
|
874
|
+
def configure_simulation(time_duration: float = 0.5,
|
|
875
|
+
time_step: float = 10.0,
|
|
876
|
+
sample_step: float = 50.0,
|
|
877
|
+
output_filename: Optional[str] = None) -> Dict[str, Any]:
|
|
878
|
+
"""配置当前工程的仿真参数。
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
time_duration: 仿真时长(秒)。
|
|
882
|
+
time_step: EMTDC 步长(微秒)。短线路须 < 传播时间(1km≈3.415µs)。
|
|
883
|
+
sample_step: 输出采样步长(微秒)。
|
|
884
|
+
output_filename: 输出文件名(不含路径,写入 .gf46 目录)。
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
success: data 含 ``parameters``(生效配置)。
|
|
888
|
+
error: 未加载工程。
|
|
889
|
+
"""
|
|
890
|
+
try:
|
|
891
|
+
if _prj is None:
|
|
892
|
+
return _err('未加载工程,请先调用 connect_and_load_project()')
|
|
893
|
+
out = output_filename or ('%s.out' % _prj_name)
|
|
894
|
+
_prj.parameters(time_duration=time_duration, time_step=time_step,
|
|
895
|
+
sample_step=sample_step, PlotType=1,
|
|
896
|
+
output_filename=out)
|
|
897
|
+
return _ok({'parameters': {'time_duration': time_duration,
|
|
898
|
+
'time_step': time_step,
|
|
899
|
+
'sample_step': sample_step,
|
|
900
|
+
'output_filename': out}})
|
|
901
|
+
except Exception as exc:
|
|
902
|
+
return _err(exc)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def run_simulation_and_monitor(timeout_loops: int = 60,
|
|
906
|
+
poll_interval: float = 1.2) -> Dict[str, Any]:
|
|
907
|
+
"""编译并运行 EMTDC 仿真,全程监控并返回底层消息。
|
|
908
|
+
|
|
909
|
+
流程:save → build → 收集 error/warning 消息 → run →
|
|
910
|
+
轮询 ``prj.output()`` 中的 'Status code = N' 直至出现或超时。
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
timeout_loops: 轮询次数上限(配合 poll_interval 即总等待上限)。
|
|
914
|
+
poll_interval: 轮询间隔(秒)。
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
success: data 含 ``status``('success'/'build_failed'/'run_finished'/
|
|
918
|
+
'timeout')、``status_code``、``errors``、``warnings``。
|
|
919
|
+
error: 未加载工程/调用异常。
|
|
920
|
+
"""
|
|
921
|
+
try:
|
|
922
|
+
if _prj is None:
|
|
923
|
+
return _err('未加载工程,请先调用 connect_and_load_project()')
|
|
924
|
+
_prj.save()
|
|
925
|
+
_prj.build()
|
|
926
|
+
|
|
927
|
+
def collect(level: str) -> List[Dict[str, str]]:
|
|
928
|
+
out = []
|
|
929
|
+
try:
|
|
930
|
+
for m in _prj.messages():
|
|
931
|
+
if m.status == level:
|
|
932
|
+
out.append({'text': m.text, 'name': m.name,
|
|
933
|
+
'label': m.label})
|
|
934
|
+
except Exception:
|
|
935
|
+
pass
|
|
936
|
+
return out
|
|
937
|
+
|
|
938
|
+
errors = collect('error')
|
|
939
|
+
warnings = collect('warning')
|
|
940
|
+
if errors:
|
|
941
|
+
return _ok({'status': 'build_failed', 'status_code': None,
|
|
942
|
+
'errors': errors, 'warnings': warnings})
|
|
943
|
+
|
|
944
|
+
_prj.run()
|
|
945
|
+
status_code: Optional[int] = None
|
|
946
|
+
for _ in range(timeout_loops):
|
|
947
|
+
time.sleep(poll_interval)
|
|
948
|
+
try:
|
|
949
|
+
m = re.search(r'Status code = (\d+)', _prj.output())
|
|
950
|
+
except Exception:
|
|
951
|
+
m = None
|
|
952
|
+
if m:
|
|
953
|
+
status_code = int(m.group(1))
|
|
954
|
+
break
|
|
955
|
+
|
|
956
|
+
run_errors = collect('error')
|
|
957
|
+
if status_code is None:
|
|
958
|
+
return _ok({'status': 'timeout', 'status_code': None,
|
|
959
|
+
'errors': run_errors, 'warnings': warnings})
|
|
960
|
+
state = 'success' if status_code == 0 else 'run_finished'
|
|
961
|
+
return _ok({'status': state, 'status_code': status_code,
|
|
962
|
+
'errors': run_errors, 'warnings': warnings})
|
|
963
|
+
except Exception as exc:
|
|
964
|
+
return _err(exc)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
# ---------------------------------------------------------------------------
|
|
968
|
+
# 工具集 5:结果读取与数值分析
|
|
969
|
+
# ---------------------------------------------------------------------------
|
|
970
|
+
def _gf_dir() -> Optional[str]:
|
|
971
|
+
"""当前工程的 .gf46 输出目录(存在性检查)。"""
|
|
972
|
+
gf = os.path.join(_prj_folder, _prj_name + '.gf46')
|
|
973
|
+
return gf if os.path.isdir(gf) else None
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def read_output(channel: Optional[str] = None,
|
|
977
|
+
t0: float = 0.0,
|
|
978
|
+
t1: Optional[float] = None,
|
|
979
|
+
method: str = 'rms',
|
|
980
|
+
freq: float = 50.0) -> Dict[str, Any]:
|
|
981
|
+
"""读取最新一次仿真的通道数据(.inf PGB 映射 + .out 列)。
|
|
982
|
+
|
|
983
|
+
格式定论:.out 每通道每相 1 列瞬时值(按 F0 频率旋转的相量投影),
|
|
984
|
+
**任意列 RMS = 相 RMS**;DFT 提取基波须用系统实际频率(F0)。
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
channel: 通道名(如 'VrA');None 返回全部通道。
|
|
988
|
+
t0, t1: 分析时间窗(秒);t1=None 取到末尾。
|
|
989
|
+
method: 'rms'(列 RMS,默认) / 'mean'(直流均值,适合 Fset) /
|
|
990
|
+
'dft'(基波 RMS,freq 参数指定频率)。
|
|
991
|
+
freq: DFT 用频率(Hz);必须等于系统实际频率,否则幅度≈0。
|
|
992
|
+
|
|
993
|
+
Returns:
|
|
994
|
+
success: data 含 ``channels``({通道: {cols: 每相值列表}})、
|
|
995
|
+
``window``、``method``。
|
|
996
|
+
error: 无输出/通道不存在。
|
|
997
|
+
"""
|
|
998
|
+
try:
|
|
999
|
+
gf = _gf_dir()
|
|
1000
|
+
if gf is None:
|
|
1001
|
+
return _err('未找到 .gf46 输出目录(工程: %s,folder: %s)——'
|
|
1002
|
+
'请先运行仿真' % (_prj_name, _prj_folder))
|
|
1003
|
+
infs = [f for f in os.listdir(gf) if f.endswith('.inf')]
|
|
1004
|
+
outs = [f for f in os.listdir(gf) if f.endswith('_01.out')]
|
|
1005
|
+
if not infs or not outs:
|
|
1006
|
+
return _err('输出文件缺失(inf=%d, out=%d)' % (len(infs), len(outs)))
|
|
1007
|
+
cmap: Dict[int, str] = {}
|
|
1008
|
+
with open(os.path.join(gf, infs[-1]), encoding='utf-8',
|
|
1009
|
+
errors='replace') as fh:
|
|
1010
|
+
for ln in fh:
|
|
1011
|
+
m = re.search(r'PGB\((\d+)\)\s+Output\s+Desc="([^"]+)"', ln)
|
|
1012
|
+
if m:
|
|
1013
|
+
cmap[int(m.group(1))] = m.group(2)
|
|
1014
|
+
rows: List[List[float]] = []
|
|
1015
|
+
with open(os.path.join(gf, outs[-1]), encoding='utf-8',
|
|
1016
|
+
errors='replace') as fh:
|
|
1017
|
+
for ln in fh:
|
|
1018
|
+
parts = ln.split()
|
|
1019
|
+
if len(parts) >= 2:
|
|
1020
|
+
try:
|
|
1021
|
+
rows.append([float(p) for p in parts])
|
|
1022
|
+
except ValueError:
|
|
1023
|
+
continue
|
|
1024
|
+
if not rows:
|
|
1025
|
+
return _err('.out 为空(仿真可能未产生数据)')
|
|
1026
|
+
cols: Dict[str, List[List[float]]] = {}
|
|
1027
|
+
for ci, cn in cmap.items():
|
|
1028
|
+
if ci < len(rows[0]):
|
|
1029
|
+
cols.setdefault(cn, []).append([r[ci] for r in rows])
|
|
1030
|
+
|
|
1031
|
+
if t1 is None:
|
|
1032
|
+
t1 = rows[-1][0]
|
|
1033
|
+
|
|
1034
|
+
def window_vals(vals: List[float]) -> List[float]:
|
|
1035
|
+
return [v for r, v in zip(rows, vals) if t0 <= r[0] <= t1]
|
|
1036
|
+
|
|
1037
|
+
result: Dict[str, Any] = {}
|
|
1038
|
+
for cn, col_list in cols.items():
|
|
1039
|
+
if channel is not None and cn != channel:
|
|
1040
|
+
continue
|
|
1041
|
+
per_phase = []
|
|
1042
|
+
for col in col_list:
|
|
1043
|
+
w = window_vals(col)
|
|
1044
|
+
if not w:
|
|
1045
|
+
per_phase.append(None)
|
|
1046
|
+
elif method == 'rms':
|
|
1047
|
+
per_phase.append(math.sqrt(sum(x * x for x in w) / len(w)))
|
|
1048
|
+
elif method == 'mean':
|
|
1049
|
+
per_phase.append(sum(w) / len(w))
|
|
1050
|
+
elif method == 'dft':
|
|
1051
|
+
re_, im = 0.0, 0.0
|
|
1052
|
+
n = len(w)
|
|
1053
|
+
for i, v in enumerate(w):
|
|
1054
|
+
a = 2 * math.pi * freq * (t0 + i * (t1 - t0) / max(n, 1) - t0)
|
|
1055
|
+
re_ += v * math.cos(a)
|
|
1056
|
+
im -= v * math.sin(a)
|
|
1057
|
+
ph = abs(complex(2 * re_ / n, 2 * im / n)) / math.sqrt(2)
|
|
1058
|
+
per_phase.append(round(ph, 6))
|
|
1059
|
+
else:
|
|
1060
|
+
return _err('未知 method=%s' % method)
|
|
1061
|
+
result[cn] = {'cols': per_phase}
|
|
1062
|
+
if channel is not None and channel not in result:
|
|
1063
|
+
return _err('通道 "%s" 不存在;可用: %s' % (channel, sorted(cols)))
|
|
1064
|
+
return _ok({'channels': result, 'window': (t0, t1), 'method': method})
|
|
1065
|
+
except Exception as exc:
|
|
1066
|
+
return _err(exc)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def compute_sequence(ia: float, ib: float, ic: float) -> Dict[str, Any]:
|
|
1070
|
+
"""Fortescue 对称分量换算(输入为基波相量幅值,输出 RMS 幅值)。
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
ia, ib, ic: 三相基波幅值(同单位;相位缺失时假定 a=0°、b=-120°、c=120°)。
|
|
1074
|
+
|
|
1075
|
+
Returns:
|
|
1076
|
+
success: data 含 ``I1``/``I2``/``I0``(RMS) 与比值 ``I2/I1``、``I0/I1``。
|
|
1077
|
+
注:严格版请用 read_output(method='dft') 取三相后再按相角合成。
|
|
1078
|
+
"""
|
|
1079
|
+
try:
|
|
1080
|
+
import cmath
|
|
1081
|
+
a = cmath.exp(2j * math.pi / 3)
|
|
1082
|
+
v1 = (ia + a * ib + a * a * ic) / 3
|
|
1083
|
+
v2 = (ia + a * a * ib + a * ic) / 3
|
|
1084
|
+
v0 = (ia + ib + ic) / 3
|
|
1085
|
+
m1 = abs(v1) / math.sqrt(2)
|
|
1086
|
+
m2 = abs(v2) / math.sqrt(2)
|
|
1087
|
+
m0 = abs(v0) / math.sqrt(2)
|
|
1088
|
+
return _ok({'I1': round(m1, 6), 'I2': round(m2, 6), 'I0': round(m0, 6),
|
|
1089
|
+
'I2/I1': round(m2 / m1, 4) if m1 > 1e-9 else None,
|
|
1090
|
+
'I0/I1': round(m0 / m1, 4) if m1 > 1e-9 else None})
|
|
1091
|
+
except Exception as exc:
|
|
1092
|
+
return _err(exc)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
# ---------------------------------------------------------------------------
|
|
1096
|
+
# 便捷构建块(高频组合逻辑,Name 寻址)
|
|
1097
|
+
# ---------------------------------------------------------------------------
|
|
1098
|
+
def build_droop_chain(gfm_name: str, m: float = 0.01, t_lp: float = 0.3,
|
|
1099
|
+
anchor_x: int = 300, anchor_y: int = 20,
|
|
1100
|
+
ph: str = 'A') -> Dict[str, Any]:
|
|
1101
|
+
"""搭建 P-f 下垂控制链(L15 精确布局)。
|
|
1102
|
+
|
|
1103
|
+
链:mult(Ig×Vg) → 一阶(realpole) → 下垂(realpole) → sumjct(50−mP) → Fset 信号。
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
gfm_name: 已存在的 source_1(EXTERNAL) 组件 Name(信号接到其 freq 端口)。
|
|
1107
|
+
m: 下垂系数(Hz/MW)。**符号约定**:Pm=Ig×Vg 且 Ig 从 GFM 流出为正时
|
|
1108
|
+
用 **+m**(本库约定);若测量方向相反需取 -m。
|
|
1109
|
+
t_lp: 一阶惯性时间常数(秒)。
|
|
1110
|
+
anchor_x, anchor_y: 控制链锚点。
|
|
1111
|
+
ph: 相位后缀(信号名后缀,避免三相同名冲突)。
|
|
1112
|
+
|
|
1113
|
+
Returns:
|
|
1114
|
+
success: data 含 ``signal``(Fset 信号名)、``components``(链内组件 Name 列表)。
|
|
1115
|
+
"""
|
|
1116
|
+
try:
|
|
1117
|
+
bad = _require_canvas()
|
|
1118
|
+
if bad:
|
|
1119
|
+
return bad
|
|
1120
|
+
sig_fk = 'Fk' + ph
|
|
1121
|
+
sig_fs = 'Fset' + ph
|
|
1122
|
+
r = get_component(gfm_name)
|
|
1123
|
+
if r['status'] != 'success':
|
|
1124
|
+
return r
|
|
1125
|
+
ps = r['data']['component'].ports()
|
|
1126
|
+
if 'freq' not in ps:
|
|
1127
|
+
return _err('组件 "%s" 无 freq 端口(需 source_1 EXTERNAL)' % gfm_name)
|
|
1128
|
+
|
|
1129
|
+
names = []
|
|
1130
|
+
r = add_and_configure_component('master:mult', anchor_x, anchor_y,
|
|
1131
|
+
name='MUL' + ph)
|
|
1132
|
+
names.append(r['data']['name'])
|
|
1133
|
+
r = add_and_configure_component('master:realpole', anchor_x + 4, anchor_y,
|
|
1134
|
+
params={'T': t_lp, 'G': 1.0, 'YO': 0.0},
|
|
1135
|
+
name='LP' + ph)
|
|
1136
|
+
names.append(r['data']['name'])
|
|
1137
|
+
r = add_and_configure_component('master:realpole', anchor_x, anchor_y - 35,
|
|
1138
|
+
params={'T': 0.001, 'G': m, 'YO': 0.0},
|
|
1139
|
+
name='MG' + ph)
|
|
1140
|
+
names.append(r['data']['name'])
|
|
1141
|
+
r = add_and_configure_component('master:sumjct', anchor_x + 6, anchor_y - 35,
|
|
1142
|
+
params={'A': 'DISABLE', 'B': 'DISABLE',
|
|
1143
|
+
'F': 'SUBTRACT', 'Dim': 1,
|
|
1144
|
+
'DPath': 'REAL'},
|
|
1145
|
+
name='SJ' + ph)
|
|
1146
|
+
names.append(r['data']['name'])
|
|
1147
|
+
r = add_and_configure_component('master:const', anchor_x + 2, anchor_y - 35,
|
|
1148
|
+
params={'Value': 50.0, 'Dim': 1},
|
|
1149
|
+
name='C50' + ph)
|
|
1150
|
+
names.append(r['data']['name'])
|
|
1151
|
+
# 信号桥:Pm 由外部接入 mult;Fk/Fset 用 datalabel 双端
|
|
1152
|
+
r = connect_signal_by_name(names[2], 'O', sig_fk, names[3], 'INF')
|
|
1153
|
+
r = connect_signal_by_name(names[4], 'OUT', sig_fs, gfm_name, 'freq')
|
|
1154
|
+
return _ok({'signal': sig_fs, 'components': names,
|
|
1155
|
+
'chain_anchor': (anchor_x, anchor_y)})
|
|
1156
|
+
except Exception as exc:
|
|
1157
|
+
return _err(exc)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
# ---------------------------------------------------------------------------
|
|
1161
|
+
# 模块自检(不连接 PSCAD,不触发懒加载)
|
|
1162
|
+
# ---------------------------------------------------------------------------
|
|
1163
|
+
def self_check() -> Dict[str, Any]:
|
|
1164
|
+
"""库自检:配置状态、导入可用性、路径存在性(不连接 PSCAD)。
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
success: data 含 ``config``(当前配置)、``config_exists``、
|
|
1168
|
+
``mhi_import``(懒加载试导入结果)、``pscad_exe_exists``、
|
|
1169
|
+
``pylib_dir_exists``、``functions``(全部工具名)。
|
|
1170
|
+
"""
|
|
1171
|
+
try:
|
|
1172
|
+
cfg = load_config()
|
|
1173
|
+
exe = _cfg_exe()
|
|
1174
|
+
pylib = _cfg_pylib()
|
|
1175
|
+
ok_import, msg = _import_mhi()
|
|
1176
|
+
return _ok({
|
|
1177
|
+
'config': cfg,
|
|
1178
|
+
'config_exists': bool(cfg),
|
|
1179
|
+
'config_path': _config_path(),
|
|
1180
|
+
'resolved': {'pscad_exe_path': exe, 'mhi_pylib_path': pylib,
|
|
1181
|
+
'pscad_port': PSCAD_PORT},
|
|
1182
|
+
'mhi_import': ok_import,
|
|
1183
|
+
'mhi_error': '' if ok_import else msg,
|
|
1184
|
+
'pscad_exe_exists': bool(exe) and os.path.isfile(exe),
|
|
1185
|
+
'pylib_dir_exists': bool(pylib) and os.path.isdir(pylib),
|
|
1186
|
+
'functions': ['init_pscad_environment', 'connect_and_load_project',
|
|
1187
|
+
'add_and_configure_component', 'connect_ports_safely',
|
|
1188
|
+
'connect_signal_by_name', 'register_pgb',
|
|
1189
|
+
'configure_simulation', 'run_simulation_and_monitor',
|
|
1190
|
+
'read_output', 'restart_pscad',
|
|
1191
|
+
'check_component_overlaps', 'list_components',
|
|
1192
|
+
'build_droop_chain'],
|
|
1193
|
+
})
|
|
1194
|
+
except Exception as exc:
|
|
1195
|
+
return _err(exc)
|