gridstate 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gridstate/__init__.py +84 -0
- gridstate/__pyinstaller/__init__.py +5 -0
- gridstate/__pyinstaller/hook-gridstate.py +5 -0
- gridstate/_version.py +24 -0
- gridstate/adapters/__init__.py +16 -0
- gridstate/adapters/from_pandapower.py +406 -0
- gridstate/algebra/__init__.py +5 -0
- gridstate/algebra/base.py +614 -0
- gridstate/algebra/estimators.py +47 -0
- gridstate/algorithms/__init__.py +5 -0
- gridstate/algorithms/ipm.py +496 -0
- gridstate/algorithms/wls.py +549 -0
- gridstate/api.py +502 -0
- gridstate/constants.py +62 -0
- gridstate/contract/__init__.py +89 -0
- gridstate/contract/derived.py +39 -0
- gridstate/contract/runtime.py +220 -0
- gridstate/contract/serialize.py +173 -0
- gridstate/contract/tables.py +641 -0
- gridstate/contract/validate.py +185 -0
- gridstate/contract/version.py +103 -0
- gridstate/losses.py +365 -0
- gridstate/pipeline.py +1102 -0
- gridstate/post_processing.py +647 -0
- gridstate/preprocessing/__init__.py +47 -0
- gridstate/preprocessing/chain_voltage.py +205 -0
- gridstate/preprocessing/dead_gen_nodes.py +101 -0
- gridstate/preprocessing/ipm_setup.py +466 -0
- gridstate/preprocessing/mirror_voltage.py +186 -0
- gridstate/preprocessing/node_props.py +92 -0
- gridstate/preprocessing/one_sided.py +232 -0
- gridstate/preprocessing/pseudo_measurements.py +517 -0
- gridstate/preprocessing/synth_injection.py +397 -0
- gridstate/quality_summary.py +202 -0
- gridstate/result.py +266 -0
- gridstate/state.py +311 -0
- gridstate/telemetry/__init__.py +89 -0
- gridstate/telemetry/_specs.py +100 -0
- gridstate/telemetry/generators.py +165 -0
- gridstate/telemetry/loss_filter.py +451 -0
- gridstate/telemetry/measurements.py +189 -0
- gridstate/telemetry/on_line.py +115 -0
- gridstate/telemetry/quality.py +159 -0
- gridstate/telemetry/rpn.py +205 -0
- gridstate/telemetry/shunts.py +209 -0
- gridstate/telemetry/topology.py +41 -0
- gridstate/telemetry/units.py +87 -0
- gridstate/telemetry/voltage_filter.py +331 -0
- gridstate/telemetry/voltage_nominal.py +44 -0
- gridstate/telemetry/xml_args.py +738 -0
- gridstate/topology.py +363 -0
- gridstate/units.py +456 -0
- gridstate/validation/__init__.py +3 -0
- gridstate/validation/_diagnostics.py +101 -0
- gridstate/validation/bad_data.py +322 -0
- gridstate/validation/chi2_test.py +130 -0
- gridstate/validation/observability.py +134 -0
- gridstate/working.py +409 -0
- gridstate/ybus.py +139 -0
- gridstate/z_vector.py +325 -0
- gridstate-0.1.0.dist-info/METADATA +199 -0
- gridstate-0.1.0.dist-info/RECORD +66 -0
- gridstate-0.1.0.dist-info/WHEEL +5 -0
- gridstate-0.1.0.dist-info/entry_points.txt +2 -0
- gridstate-0.1.0.dist-info/licenses/LICENSE +91 -0
- gridstate-0.1.0.dist-info/top_level.txt +1 -0
gridstate/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""gridstate — power system State Estimation.
|
|
2
|
+
|
|
3
|
+
The public API is gathered here; see the README for an overview and the
|
|
4
|
+
individual function docstrings for usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
from gridstate.api import estimate
|
|
10
|
+
from gridstate.contract import (
|
|
11
|
+
SEInput,
|
|
12
|
+
SEOutput,
|
|
13
|
+
load_se_input,
|
|
14
|
+
)
|
|
15
|
+
from gridstate.contract import (
|
|
16
|
+
run as run_se,
|
|
17
|
+
)
|
|
18
|
+
from gridstate.contract.serialize import (
|
|
19
|
+
load_se_input_npz,
|
|
20
|
+
save_se_input,
|
|
21
|
+
)
|
|
22
|
+
from gridstate.pipeline import (
|
|
23
|
+
PipelineConfig,
|
|
24
|
+
)
|
|
25
|
+
from gridstate.pipeline import (
|
|
26
|
+
manifest as pipeline_manifest,
|
|
27
|
+
)
|
|
28
|
+
from gridstate.pipeline import (
|
|
29
|
+
run as run_pipeline,
|
|
30
|
+
)
|
|
31
|
+
from gridstate.result import (
|
|
32
|
+
Chi2Summary,
|
|
33
|
+
ImbalanceRow,
|
|
34
|
+
ResidualRow,
|
|
35
|
+
SEResult,
|
|
36
|
+
)
|
|
37
|
+
from gridstate.validation.bad_data import (
|
|
38
|
+
BadDataResult,
|
|
39
|
+
NormalizedResidualReport,
|
|
40
|
+
compute_normalized_residuals_report,
|
|
41
|
+
remove_bad_data,
|
|
42
|
+
)
|
|
43
|
+
from gridstate.validation.chi2_test import Chi2Result, chi2_analysis
|
|
44
|
+
from gridstate.validation.observability import (
|
|
45
|
+
ObservabilityReport,
|
|
46
|
+
analyze_observability,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Version is derived from git tags by setuptools_scm and baked into the
|
|
52
|
+
# package metadata at build/install time; read it back from there at
|
|
53
|
+
# runtime. The fallback only triggers in a bare source tree that was never
|
|
54
|
+
# installed or built.
|
|
55
|
+
__version__ = version("gridstate")
|
|
56
|
+
except PackageNotFoundError: # pragma: no cover - un-installed source tree
|
|
57
|
+
__version__ = "0+unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"BadDataResult",
|
|
62
|
+
"Chi2Result",
|
|
63
|
+
"Chi2Summary",
|
|
64
|
+
"ImbalanceRow",
|
|
65
|
+
"NormalizedResidualReport",
|
|
66
|
+
"ObservabilityReport",
|
|
67
|
+
"PipelineConfig",
|
|
68
|
+
"ResidualRow",
|
|
69
|
+
"SEInput",
|
|
70
|
+
"SEOutput",
|
|
71
|
+
"SEResult",
|
|
72
|
+
"__version__",
|
|
73
|
+
"analyze_observability",
|
|
74
|
+
"chi2_analysis",
|
|
75
|
+
"compute_normalized_residuals_report",
|
|
76
|
+
"estimate",
|
|
77
|
+
"load_se_input",
|
|
78
|
+
"load_se_input_npz",
|
|
79
|
+
"pipeline_manifest",
|
|
80
|
+
"remove_bad_data",
|
|
81
|
+
"run_pipeline",
|
|
82
|
+
"run_se",
|
|
83
|
+
"save_se_input",
|
|
84
|
+
]
|
gridstate/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Адаптеры внешних форматов сети → контракт gridstate (``SEInput``).
|
|
2
|
+
|
|
3
|
+
Адаптеры — **не** часть рантайма SE: они конвертируют чужой формат описания
|
|
4
|
+
сети (например, ``pandapower``-``net``) в контрактные структурированные массивы
|
|
5
|
+
:data:`gridstate.contract.tables.SE_INPUT` и оборачивают их в ``SEInput`` через
|
|
6
|
+
:meth:`gridstate.working.Working.from_arrays`. Сам пакет ``gridstate`` остаётся
|
|
7
|
+
numpy/scipy-only; внешняя библиотека (pandapower) импортируется **лениво** внутри
|
|
8
|
+
адаптера и нужна лишь тому, кто строит фикстуры (dev-extra ``[test-models]``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from gridstate.adapters.from_pandapower import from_pandapower, measurement_array
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["from_pandapower", "measurement_array"]
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Адаптер ``pandapower.net`` → контракт gridstate (``SEInput``).
|
|
2
|
+
|
|
3
|
+
Конвертирует описание сети из ``pandapower`` в контрактные структурированные
|
|
4
|
+
массивы (:data:`gridstate.contract.tables.SE_INPUT`) и оборачивает их в
|
|
5
|
+
:class:`~gridstate.contract.runtime.SEInput` (``derived=None`` — формат-зависимые
|
|
6
|
+
шаги пайплайна пропускаются: модель уже несёт измерения/режим).
|
|
7
|
+
|
|
8
|
+
Единицы (контракт; перевод в p.u. делает :mod:`gridstate.units`):
|
|
9
|
+
|
|
10
|
+
* ``BASE_MVA = 100`` фиксирована;
|
|
11
|
+
* импеданс ветвей — в **Омах**, приведённый к стороне ``from`` (для линии — bus
|
|
12
|
+
``from``, для трансформатора — bus ``hv``);
|
|
13
|
+
* проводимости/восприимчивости (и шунты узлов, и зарядная b ветвей) — в
|
|
14
|
+
**Сименсах**;
|
|
15
|
+
* ``voltage_nominal`` — в кВ; узловые инжекции generation/load — в МВт/МВАр.
|
|
16
|
+
|
|
17
|
+
``pandapower`` импортируется лениво: пакет ``gridstate`` numpy/scipy-only, а
|
|
18
|
+
pandapower нужен только тому, кто строит фикстуры (dev-extra ``[test-models]``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from gridstate.constants import BranchType, NodeType
|
|
29
|
+
from gridstate.contract.runtime import SEInput
|
|
30
|
+
from gridstate.contract.tables import SE_INPUT, SE_OUTPUT
|
|
31
|
+
from gridstate.working import Working
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import pandapower as pp # noqa: F401
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_PP_IMPORT_HINT = (
|
|
39
|
+
"Для работы адаптера from_pandapower нужен пакет pandapower. Он не входит в "
|
|
40
|
+
"рантайм-зависимости gridstate (numpy/scipy-only); установите dev-extra:\n"
|
|
41
|
+
" pip install 'gridstate[test-models]'\n"
|
|
42
|
+
"pandapower нужен только для ГЕНЕРАЦИИ фикстур, не для прогона SE."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _require_pandapower() -> Any:
|
|
47
|
+
"""Ленивый guarded-импорт pandapower (подсказка про extra при отсутствии)."""
|
|
48
|
+
try:
|
|
49
|
+
import pandapower as pp
|
|
50
|
+
except ImportError as exc: # pragma: no cover — путь без pandapower
|
|
51
|
+
raise ImportError(_PP_IMPORT_HINT) from exc
|
|
52
|
+
return pp
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _io_dtype(in_schema: Any, out_schema: Any) -> np.dtype:
|
|
56
|
+
"""Объединённый dtype входного+выходного слоёв таблицы (как ``Working.empty``).
|
|
57
|
+
|
|
58
|
+
Пайплайн пишет OUTPUT-колонки (``voltage_magnitude``/``p_inj_calc``/перетоки/
|
|
59
|
+
``estimated_si`` …) прямо в живые коллекции, поэтому backing-массивы должны
|
|
60
|
+
нести и INPUT/WORKING, и OUTPUT-колонки — ровно как нулевые коллекции
|
|
61
|
+
:meth:`gridstate.working.Working.empty`.
|
|
62
|
+
"""
|
|
63
|
+
in_dt = in_schema.input_dtype()
|
|
64
|
+
fields = list(in_dt.descr)
|
|
65
|
+
have = set(in_dt.names or ())
|
|
66
|
+
out_dt = out_schema.output_dtype()
|
|
67
|
+
for name in out_dt.names or ():
|
|
68
|
+
if name not in have:
|
|
69
|
+
fields.append((name, out_dt[name].str))
|
|
70
|
+
return np.dtype(fields)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_NODES_DTYPE = _io_dtype(SE_INPUT.nodes, SE_OUTPUT.nodes)
|
|
74
|
+
_BRANCHES_DTYPE = _io_dtype(SE_INPUT.branches, SE_OUTPUT.branches)
|
|
75
|
+
_MEASUREMENTS_DTYPE = _io_dtype(SE_INPUT.measurements, SE_OUTPUT.measurements)
|
|
76
|
+
_GENERATORS_DTYPE = SE_INPUT.generators.input_dtype()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def from_pandapower(
|
|
80
|
+
net: Any,
|
|
81
|
+
*,
|
|
82
|
+
measurements: np.ndarray | None = None,
|
|
83
|
+
) -> SEInput:
|
|
84
|
+
"""Построить ``SEInput`` из ``pandapower``-сети ``net``.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
net: ``pandapower.auxiliary.pandapowerNet`` — описание сети. Должен иметь
|
|
88
|
+
ровно один ``ext_grid`` (он становится slack-узлом). ``net.sn_mva``
|
|
89
|
+
ожидается равным 100 (контрактная база); иначе бросается ``ValueError``.
|
|
90
|
+
measurements: опциональный контрактный структурный массив измерений
|
|
91
|
+
(dtype = ``SE_INPUT.measurements.input_dtype()``). Если ``None`` —
|
|
92
|
+
таблица измерений пустая (генератор фикстур добавит синтетический
|
|
93
|
+
z-вектор из решения PF).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
:class:`SEInput` с ``derived=None`` (модель уже несёт узлы/ветви/режим/
|
|
97
|
+
измерения; XML/формат-зависимые шаги пайплайна пропускаются).
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ImportError: если pandapower не установлен (см. extra ``[test-models]``).
|
|
101
|
+
ValueError: при ≠1 ext_grid, ``net.sn_mva ≠ 100`` или пустой сети.
|
|
102
|
+
"""
|
|
103
|
+
_require_pandapower() # подсказка про extra; ниже работаем только с net-таблицами
|
|
104
|
+
|
|
105
|
+
sn_mva = float(getattr(net, "sn_mva", 100.0))
|
|
106
|
+
if not math.isclose(sn_mva, 100.0, rel_tol=1e-9):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"from_pandapower поддерживает только net.sn_mva == 100 (контрактная база), "
|
|
109
|
+
f"получено {sn_mva}."
|
|
110
|
+
)
|
|
111
|
+
if len(net.bus) == 0:
|
|
112
|
+
raise ValueError("Сеть пустая: нет шин (net.bus).")
|
|
113
|
+
if len(net.ext_grid) != 1:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Адаптер требует ровно один ext_grid (slack), получено {len(net.ext_grid)}."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
f_hz = float(getattr(net, "f_hz", 50.0))
|
|
119
|
+
|
|
120
|
+
nodes = _build_nodes(net)
|
|
121
|
+
branches = _build_branches(net, f_hz)
|
|
122
|
+
generators = _build_generators(net)
|
|
123
|
+
meas = _coerce_measurements(measurements)
|
|
124
|
+
|
|
125
|
+
working = Working.from_arrays(
|
|
126
|
+
nodes=nodes,
|
|
127
|
+
branches=branches,
|
|
128
|
+
measurements=meas,
|
|
129
|
+
generators=generators,
|
|
130
|
+
)
|
|
131
|
+
return SEInput(model=working, derived=None)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _coerce_measurements(measurements: np.ndarray | None) -> np.ndarray:
|
|
135
|
+
"""Привести измерения к dtype рабочего слоя (INPUT⊕OUTPUT-колонки).
|
|
136
|
+
|
|
137
|
+
Принимает массив контрактного INPUT-dtype (или уже I/O-dtype) и копирует
|
|
138
|
+
его поля в массив :data:`_MEASUREMENTS_DTYPE` (с OUTPUT-колонками, которые
|
|
139
|
+
пайплайн заполняет). ``None`` → пустая таблица.
|
|
140
|
+
"""
|
|
141
|
+
if measurements is None:
|
|
142
|
+
return np.zeros(0, dtype=_MEASUREMENTS_DTYPE)
|
|
143
|
+
src = np.asarray(measurements)
|
|
144
|
+
out = np.zeros(len(src), dtype=_MEASUREMENTS_DTYPE)
|
|
145
|
+
for name in src.dtype.names or ():
|
|
146
|
+
if name in (out.dtype.names or ()):
|
|
147
|
+
out[name] = src[name]
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def measurement_array(n: int) -> np.ndarray:
|
|
152
|
+
"""Пустой (нулевой) контрактный массив измерений длины ``n`` для заполнения.
|
|
153
|
+
|
|
154
|
+
Хелпер для построителей фикстур: возвращает массив dtype рабочего слоя
|
|
155
|
+
(INPUT⊕OUTPUT), поля ``status``/``quality`` остаются дефолтными — заполните
|
|
156
|
+
``id``/``object_type``/``object_id``/``measurement_type``/``value``/
|
|
157
|
+
``variance``/``branch_side``/``status``.
|
|
158
|
+
"""
|
|
159
|
+
return np.zeros(n, dtype=_MEASUREMENTS_DTYPE)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Узлы
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_nodes(net: Any) -> np.ndarray:
|
|
168
|
+
"""``net.bus`` + инжекции (load/gen/sgen/ext_grid/shunt) → массив узлов."""
|
|
169
|
+
bus_index = list(net.bus.index)
|
|
170
|
+
n = len(bus_index)
|
|
171
|
+
pos_of = {bid: i for i, bid in enumerate(bus_index)}
|
|
172
|
+
|
|
173
|
+
arr = np.zeros(n, dtype=_NODES_DTYPE)
|
|
174
|
+
arr["id"] = np.asarray(bus_index, dtype=np.int64)
|
|
175
|
+
arr["voltage_nominal"] = net.bus.vn_kv.to_numpy(dtype=np.float64)
|
|
176
|
+
arr["status"] = net.bus.in_service.to_numpy(dtype=bool)
|
|
177
|
+
arr["node_type"] = int(NodeType.PQ)
|
|
178
|
+
# Плоский старт: V = Vном, δ = 0 (солвер перезапишет).
|
|
179
|
+
arr["voltage_magnitude"] = arr["voltage_nominal"]
|
|
180
|
+
arr["voltage_angle"] = 0.0
|
|
181
|
+
|
|
182
|
+
has_gen = np.zeros(n, dtype=bool)
|
|
183
|
+
has_load = np.zeros(n, dtype=bool)
|
|
184
|
+
|
|
185
|
+
# Нагрузка (consumer, +).
|
|
186
|
+
if len(net.load) > 0:
|
|
187
|
+
for bus, p, q, in_svc in zip(
|
|
188
|
+
net.load.bus, net.load.p_mw, net.load.q_mvar, net.load.in_service, strict=False
|
|
189
|
+
):
|
|
190
|
+
if not bool(in_svc):
|
|
191
|
+
continue
|
|
192
|
+
i = pos_of[bus]
|
|
193
|
+
arr["load_p"][i] += float(p)
|
|
194
|
+
arr["load_q"][i] += float(q)
|
|
195
|
+
has_load[i] = True
|
|
196
|
+
|
|
197
|
+
# Генерация PV (net.gen): задаёт node_type=PV (если узел не slack).
|
|
198
|
+
if len(net.gen) > 0:
|
|
199
|
+
for bus, p, in_svc in zip(net.gen.bus, net.gen.p_mw, net.gen.in_service, strict=False):
|
|
200
|
+
if not bool(in_svc):
|
|
201
|
+
continue
|
|
202
|
+
i = pos_of[bus]
|
|
203
|
+
arr["generation_p"][i] += float(p)
|
|
204
|
+
has_gen[i] = True
|
|
205
|
+
arr["node_type"][i] = int(NodeType.PV)
|
|
206
|
+
# Vsetpoint для PV (vm_pu задан на gen).
|
|
207
|
+
if "vm_pu" in net.gen.columns:
|
|
208
|
+
for bus, vm, in_svc in zip(
|
|
209
|
+
net.gen.bus, net.gen.vm_pu, net.gen.in_service, strict=False
|
|
210
|
+
):
|
|
211
|
+
if not bool(in_svc):
|
|
212
|
+
continue
|
|
213
|
+
i = pos_of[bus]
|
|
214
|
+
arr["voltage_setpoint"][i] = float(vm) * float(arr["voltage_nominal"][i])
|
|
215
|
+
|
|
216
|
+
# Статическая генерация (net.sgen) — PQ-инжекция (+).
|
|
217
|
+
if len(net.sgen) > 0:
|
|
218
|
+
for bus, p, q, in_svc in zip(
|
|
219
|
+
net.sgen.bus, net.sgen.p_mw, net.sgen.q_mvar, net.sgen.in_service, strict=False
|
|
220
|
+
):
|
|
221
|
+
if not bool(in_svc):
|
|
222
|
+
continue
|
|
223
|
+
i = pos_of[bus]
|
|
224
|
+
arr["generation_p"][i] += float(p)
|
|
225
|
+
arr["generation_q"][i] += float(q)
|
|
226
|
+
has_gen[i] = True
|
|
227
|
+
|
|
228
|
+
# Шунты (net.shunt): q_mvar/p_mw заданы при vn_kv шунта; переводим в См.
|
|
229
|
+
# B[См] = -Q_MVAr / (vn_shunt_kv² ) ; знак: Q_MVAr>0 = поглощение реактива
|
|
230
|
+
# (индуктивный) → отрицательная susceptance. P_MW>0 = активные потери → G>0.
|
|
231
|
+
if len(net.shunt) > 0:
|
|
232
|
+
for bus, p_mw, q_mvar, vn_kv, step, in_svc in zip(
|
|
233
|
+
net.shunt.bus,
|
|
234
|
+
net.shunt.p_mw,
|
|
235
|
+
net.shunt.q_mvar,
|
|
236
|
+
net.shunt.vn_kv,
|
|
237
|
+
net.shunt.step,
|
|
238
|
+
net.shunt.in_service,
|
|
239
|
+
strict=False,
|
|
240
|
+
):
|
|
241
|
+
if not bool(in_svc):
|
|
242
|
+
continue
|
|
243
|
+
i = pos_of[bus]
|
|
244
|
+
vn = float(vn_kv)
|
|
245
|
+
if vn <= 0:
|
|
246
|
+
continue
|
|
247
|
+
s = float(step)
|
|
248
|
+
arr["shunt_g"][i] += s * float(p_mw) / (vn * vn)
|
|
249
|
+
arr["shunt_b"][i] += -s * float(q_mvar) / (vn * vn)
|
|
250
|
+
|
|
251
|
+
# ext_grid → slack-узел; несёт генерацию-баланс (P/Q не известны до PF — 0).
|
|
252
|
+
eg_bus = int(net.ext_grid.bus.iloc[0])
|
|
253
|
+
i_slack = pos_of[eg_bus]
|
|
254
|
+
arr["node_type"][i_slack] = int(NodeType.SLACK)
|
|
255
|
+
has_gen[i_slack] = True
|
|
256
|
+
if "vm_pu" in net.ext_grid.columns:
|
|
257
|
+
vm = float(net.ext_grid.vm_pu.iloc[0])
|
|
258
|
+
arr["voltage_setpoint"][i_slack] = vm * float(arr["voltage_nominal"][i_slack])
|
|
259
|
+
arr["voltage_magnitude"][i_slack] = vm * float(arr["voltage_nominal"][i_slack])
|
|
260
|
+
|
|
261
|
+
arr["exist_gen"] = has_gen.astype(np.int8)
|
|
262
|
+
arr["exist_load"] = has_load.astype(np.int8)
|
|
263
|
+
return arr
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Ветви
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _build_branches(net: Any, f_hz: float) -> np.ndarray:
|
|
272
|
+
"""``net.line`` + ``net.trafo`` → массив ветвей (импеданс Ом, шунты См)."""
|
|
273
|
+
rows: list[dict[str, Any]] = []
|
|
274
|
+
next_bid = 1
|
|
275
|
+
|
|
276
|
+
# ----- Линии -----
|
|
277
|
+
for _, ln in net.line.iterrows():
|
|
278
|
+
length = float(ln.length_km)
|
|
279
|
+
parallel = max(int(ln.parallel), 1)
|
|
280
|
+
# Последовательный импеданс: r_ohm_per_km·length / parallel (Ом).
|
|
281
|
+
r = float(ln.r_ohm_per_km) * length / parallel
|
|
282
|
+
x = float(ln.x_ohm_per_km) * length / parallel
|
|
283
|
+
# Зарядная susceptance: B = 2π·f·C·length·parallel (См), C в нФ/км.
|
|
284
|
+
b_total = 2.0 * math.pi * f_hz * float(ln.c_nf_per_km) * 1e-9 * length * parallel
|
|
285
|
+
g_total = float(ln.g_us_per_km) * 1e-6 * length * parallel
|
|
286
|
+
rows.append(
|
|
287
|
+
{
|
|
288
|
+
"id": next_bid,
|
|
289
|
+
"from_node": int(ln.from_bus),
|
|
290
|
+
"to_node": int(ln.to_bus),
|
|
291
|
+
"branch_type": int(BranchType.LINE),
|
|
292
|
+
"status": bool(ln.in_service),
|
|
293
|
+
"resistance": r,
|
|
294
|
+
"reactance": x,
|
|
295
|
+
# Серийный шунт ветви (Π-схема): половина зарядной на каждый конец.
|
|
296
|
+
"conductance_from": g_total / 2.0,
|
|
297
|
+
"susceptance_from": b_total / 2.0,
|
|
298
|
+
"conductance_to": g_total / 2.0,
|
|
299
|
+
"susceptance_to": b_total / 2.0,
|
|
300
|
+
"tap_ratio": 1.0,
|
|
301
|
+
"phase_shift": 0.0,
|
|
302
|
+
"current_limit_normal": float(ln.max_i_ka) * 1000.0
|
|
303
|
+
if "max_i_ka" in net.line.columns and not _is_nan(ln.max_i_ka)
|
|
304
|
+
else 0.0,
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
next_bid += 1
|
|
308
|
+
|
|
309
|
+
# ----- Трансформаторы -----
|
|
310
|
+
for _, tr in net.trafo.iterrows():
|
|
311
|
+
parallel = max(int(tr.parallel), 1)
|
|
312
|
+
sn = float(tr.sn_mva)
|
|
313
|
+
vn_hv = float(tr.vn_hv_kv)
|
|
314
|
+
vn_lv = float(tr.vn_lv_kv)
|
|
315
|
+
# Короткое замыкание: z%, r% на базе sn_mva, приведено к HV-стороне.
|
|
316
|
+
z_pu = float(tr.vk_percent) / 100.0 # на базе sn_mva, vn_hv
|
|
317
|
+
r_pu = float(tr.vkr_percent) / 100.0
|
|
318
|
+
x_pu = math.sqrt(max(z_pu * z_pu - r_pu * r_pu, 0.0))
|
|
319
|
+
z_base_hv = vn_hv * vn_hv / sn # Ом (на собственной базе трафо)
|
|
320
|
+
r_ohm = r_pu * z_base_hv / parallel
|
|
321
|
+
x_ohm = x_pu * z_base_hv / parallel
|
|
322
|
+
|
|
323
|
+
# Коэф. трансформации (физический, HV:LV) с учётом РПН.
|
|
324
|
+
tap_factor = 1.0
|
|
325
|
+
if not _is_nan(getattr(tr, "tap_pos", float("nan"))):
|
|
326
|
+
tap_pos = float(tr.tap_pos)
|
|
327
|
+
tap_neutral = float(tr.tap_neutral) if not _is_nan(tr.tap_neutral) else 0.0
|
|
328
|
+
tap_step = float(tr.tap_step_percent) if not _is_nan(tr.tap_step_percent) else 0.0
|
|
329
|
+
delta = (tap_pos - tap_neutral) * tap_step / 100.0
|
|
330
|
+
tap_side = str(getattr(tr, "tap_side", "hv"))
|
|
331
|
+
# РПН на HV: ratio растёт; на LV — обратно.
|
|
332
|
+
tap_factor = (1.0 + delta) if tap_side == "hv" else 1.0 / (1.0 + delta)
|
|
333
|
+
tap_ratio = (vn_hv / vn_lv) * tap_factor
|
|
334
|
+
|
|
335
|
+
shift = float(tr.shift_degree) if not _is_nan(tr.shift_degree) else 0.0
|
|
336
|
+
|
|
337
|
+
rows.append(
|
|
338
|
+
{
|
|
339
|
+
"id": next_bid,
|
|
340
|
+
"from_node": int(tr.hv_bus),
|
|
341
|
+
"to_node": int(tr.lv_bus),
|
|
342
|
+
"branch_type": int(BranchType.TRANSFORMER),
|
|
343
|
+
"status": bool(tr.in_service),
|
|
344
|
+
"resistance": r_ohm,
|
|
345
|
+
"reactance": x_ohm,
|
|
346
|
+
# Намагничивающую ветвь (i0/pfe) опускаем: на IEEE-кейсах
|
|
347
|
+
# пренебрежимо мала, z-вектор её не несёт.
|
|
348
|
+
"tap_ratio": tap_ratio,
|
|
349
|
+
"phase_shift": math.radians(shift),
|
|
350
|
+
"current_limit_normal": 0.0,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
next_bid += 1
|
|
354
|
+
|
|
355
|
+
arr = np.zeros(len(rows), dtype=_BRANCHES_DTYPE)
|
|
356
|
+
for i, row in enumerate(rows):
|
|
357
|
+
for key, value in row.items():
|
|
358
|
+
if key in (arr.dtype.names or ()):
|
|
359
|
+
arr[i][key] = value
|
|
360
|
+
# parallel_id обязателен для KEY; единичные ветви → 1.
|
|
361
|
+
if "parallel_id" in (arr.dtype.names or ()):
|
|
362
|
+
arr["parallel_id"] = 1
|
|
363
|
+
return arr
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# Генераторы
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _build_generators(net: Any) -> np.ndarray:
|
|
372
|
+
"""``net.gen`` + ``net.sgen`` → таблица генераторов (вход контракта).
|
|
373
|
+
|
|
374
|
+
На IEEE-фикстурах генерация уже агрегирована в узловую инжекцию
|
|
375
|
+
(``node.generation_p/q``); отдельная таблица генераторов нужна контракту как
|
|
376
|
+
непустая структура, но границы box не требуются. Заполняем минимально.
|
|
377
|
+
"""
|
|
378
|
+
rows: list[tuple[int, int, float, float]] = []
|
|
379
|
+
gid = 1
|
|
380
|
+
if len(net.gen) > 0:
|
|
381
|
+
for bus, p in zip(net.gen.bus, net.gen.p_mw, strict=False):
|
|
382
|
+
rows.append((gid, int(bus), float(p), 0.0))
|
|
383
|
+
gid += 1
|
|
384
|
+
if len(net.sgen) > 0:
|
|
385
|
+
for bus, p, q in zip(net.sgen.bus, net.sgen.p_mw, net.sgen.q_mvar, strict=False):
|
|
386
|
+
rows.append((gid, int(bus), float(p), float(q)))
|
|
387
|
+
gid += 1
|
|
388
|
+
|
|
389
|
+
arr = np.zeros(len(rows), dtype=_GENERATORS_DTYPE)
|
|
390
|
+
for i, (g_id, node_id, p, q) in enumerate(rows):
|
|
391
|
+
arr[i]["id"] = g_id
|
|
392
|
+
arr[i]["node_id"] = node_id
|
|
393
|
+
arr[i]["power_output"] = p
|
|
394
|
+
arr[i]["reactive_output"] = q
|
|
395
|
+
arr[i]["status"] = True
|
|
396
|
+
return arr
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _is_nan(value: Any) -> bool:
|
|
400
|
+
"""True, если ``value`` — NaN/None (pandapower кодирует «нет значения» как NaN)."""
|
|
401
|
+
if value is None:
|
|
402
|
+
return True
|
|
403
|
+
try:
|
|
404
|
+
return bool(math.isnan(float(value)))
|
|
405
|
+
except (TypeError, ValueError):
|
|
406
|
+
return False
|