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.
Files changed (66) hide show
  1. gridstate/__init__.py +84 -0
  2. gridstate/__pyinstaller/__init__.py +5 -0
  3. gridstate/__pyinstaller/hook-gridstate.py +5 -0
  4. gridstate/_version.py +24 -0
  5. gridstate/adapters/__init__.py +16 -0
  6. gridstate/adapters/from_pandapower.py +406 -0
  7. gridstate/algebra/__init__.py +5 -0
  8. gridstate/algebra/base.py +614 -0
  9. gridstate/algebra/estimators.py +47 -0
  10. gridstate/algorithms/__init__.py +5 -0
  11. gridstate/algorithms/ipm.py +496 -0
  12. gridstate/algorithms/wls.py +549 -0
  13. gridstate/api.py +502 -0
  14. gridstate/constants.py +62 -0
  15. gridstate/contract/__init__.py +89 -0
  16. gridstate/contract/derived.py +39 -0
  17. gridstate/contract/runtime.py +220 -0
  18. gridstate/contract/serialize.py +173 -0
  19. gridstate/contract/tables.py +641 -0
  20. gridstate/contract/validate.py +185 -0
  21. gridstate/contract/version.py +103 -0
  22. gridstate/losses.py +365 -0
  23. gridstate/pipeline.py +1102 -0
  24. gridstate/post_processing.py +647 -0
  25. gridstate/preprocessing/__init__.py +47 -0
  26. gridstate/preprocessing/chain_voltage.py +205 -0
  27. gridstate/preprocessing/dead_gen_nodes.py +101 -0
  28. gridstate/preprocessing/ipm_setup.py +466 -0
  29. gridstate/preprocessing/mirror_voltage.py +186 -0
  30. gridstate/preprocessing/node_props.py +92 -0
  31. gridstate/preprocessing/one_sided.py +232 -0
  32. gridstate/preprocessing/pseudo_measurements.py +517 -0
  33. gridstate/preprocessing/synth_injection.py +397 -0
  34. gridstate/quality_summary.py +202 -0
  35. gridstate/result.py +266 -0
  36. gridstate/state.py +311 -0
  37. gridstate/telemetry/__init__.py +89 -0
  38. gridstate/telemetry/_specs.py +100 -0
  39. gridstate/telemetry/generators.py +165 -0
  40. gridstate/telemetry/loss_filter.py +451 -0
  41. gridstate/telemetry/measurements.py +189 -0
  42. gridstate/telemetry/on_line.py +115 -0
  43. gridstate/telemetry/quality.py +159 -0
  44. gridstate/telemetry/rpn.py +205 -0
  45. gridstate/telemetry/shunts.py +209 -0
  46. gridstate/telemetry/topology.py +41 -0
  47. gridstate/telemetry/units.py +87 -0
  48. gridstate/telemetry/voltage_filter.py +331 -0
  49. gridstate/telemetry/voltage_nominal.py +44 -0
  50. gridstate/telemetry/xml_args.py +738 -0
  51. gridstate/topology.py +363 -0
  52. gridstate/units.py +456 -0
  53. gridstate/validation/__init__.py +3 -0
  54. gridstate/validation/_diagnostics.py +101 -0
  55. gridstate/validation/bad_data.py +322 -0
  56. gridstate/validation/chi2_test.py +130 -0
  57. gridstate/validation/observability.py +134 -0
  58. gridstate/working.py +409 -0
  59. gridstate/ybus.py +139 -0
  60. gridstate/z_vector.py +325 -0
  61. gridstate-0.1.0.dist-info/METADATA +199 -0
  62. gridstate-0.1.0.dist-info/RECORD +66 -0
  63. gridstate-0.1.0.dist-info/WHEEL +5 -0
  64. gridstate-0.1.0.dist-info/entry_points.txt +2 -0
  65. gridstate-0.1.0.dist-info/licenses/LICENSE +91 -0
  66. 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
+ ]
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def get_hook_dirs() -> list[str]:
5
+ return [str(Path(__file__).parent)]
@@ -0,0 +1,5 @@
1
+ from PyInstaller.utils.hooks import collect_data_files, collect_submodules
2
+
3
+
4
+ hiddenimports = collect_submodules("gridstate")
5
+ datas = collect_data_files("gridstate")
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
@@ -0,0 +1,5 @@
1
+ """Алгебра SE: функция измерений h(E), якобиан H, cost-функции.
2
+
3
+ Изолирована от конкретного алгоритма — ``gridstate/algorithms/*`` используют
4
+ этот слой как строительный блок.
5
+ """