load-flow-engine 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- load_flow_engine-0.1.0/PKG-INFO +12 -0
- load_flow_engine-0.1.0/README.md +168 -0
- load_flow_engine-0.1.0/load_flow_engine/__init__.py +42 -0
- load_flow_engine-0.1.0/load_flow_engine/constants.py +12 -0
- load_flow_engine-0.1.0/load_flow_engine/enums.py +24 -0
- load_flow_engine-0.1.0/load_flow_engine/example.py +114 -0
- load_flow_engine-0.1.0/load_flow_engine/helpers.py +84 -0
- load_flow_engine-0.1.0/load_flow_engine/models.py +219 -0
- load_flow_engine-0.1.0/load_flow_engine/network.py +256 -0
- load_flow_engine-0.1.0/load_flow_engine/solver.py +546 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/cim_adapter.py +720 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/cyme_adapter.py +545 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/__init__.py +194 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_common.py +163 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_controls.py +105 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_duplicates.py +136 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_grounding.py +58 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_impedance.py +94 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_load_model.py +144 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_open_conductor.py +132 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_phase.py +150 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_topology.py +181 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_transformer.py +104 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/diagnostics/_diag_voltage_base.py +151 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/load_allocation.py +87 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/multiconductor_adapter.py +577 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/opendss_adapter.py +322 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/output.py +89 -0
- load_flow_engine-0.1.0/load_flow_engine/tools/sqlite_adapter.py +367 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/PKG-INFO +12 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/SOURCES.txt +35 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/dependency_links.txt +1 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/entry_points.txt +2 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/requires.txt +9 -0
- load_flow_engine-0.1.0/load_flow_engine.egg-info/top_level.txt +1 -0
- load_flow_engine-0.1.0/pyproject.toml +28 -0
- load_flow_engine-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: load-flow-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Three-phase unbalanced load flow solver
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: numpy
|
|
7
|
+
Requires-Dist: scipy
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: build; extra == "dev"
|
|
10
|
+
Requires-Dist: twine; extra == "dev"
|
|
11
|
+
Provides-Extra: notebooks
|
|
12
|
+
Requires-Dist: jupyter; extra == "notebooks"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# lfe-python
|
|
2
|
+
|
|
3
|
+
`lfe-python` is a three-phase unbalanced load flow engine written in Python. The project mirrors ETAP-style data structures and workflow while exposing a small, scriptable API for building distribution networks, assembling the admittance matrix, solving load flow, and exporting results.
|
|
4
|
+
|
|
5
|
+
The package published by this repository is `load-flow-engine`, and the main Python package is `load_flow_engine`.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Three-phase unbalanced load flow using Newton-Raphson or Gauss-Seidel
|
|
10
|
+
- Sparse `Y_abc` network assembly for buses, branches, transformers, loads, generators, shunts, and switches
|
|
11
|
+
- Per-phase bus voltage and branch loading results
|
|
12
|
+
- Built-in 4-bus example feeder
|
|
13
|
+
- Backward-compatible root module via `three_phase_loadflow.py`
|
|
14
|
+
- Import/export helpers for SQLite, OpenDSS, CIM/CGMES, CYME, and pandapower-oriented conversion utilities
|
|
15
|
+
- Diagnostics utilities for common topology, impedance, grounding, and modeling issues
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Core dependencies are minimal:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Optional extras defined in `pyproject.toml`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e .[dev]
|
|
29
|
+
pip install -e .[notebooks]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Some tool modules rely on additional packages that are not currently declared as extras:
|
|
33
|
+
|
|
34
|
+
- `pandas` for `load_flow_engine.tools.output`
|
|
35
|
+
- `pandas` and `pandapower` for `load_flow_engine.tools.multiconductor_adapter`
|
|
36
|
+
- `cympy` for `load_flow_engine.tools.cyme_adapter` and a local CYME installation
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from load_flow_engine import build_example_network, ThreePhaseLoadFlowSolver
|
|
42
|
+
|
|
43
|
+
net = build_example_network()
|
|
44
|
+
solver = ThreePhaseLoadFlowSolver(net, method="nr")
|
|
45
|
+
|
|
46
|
+
converged = solver.solve()
|
|
47
|
+
print("Converged:", converged)
|
|
48
|
+
|
|
49
|
+
solver.print_bus_results()
|
|
50
|
+
branch_results = solver.compute_branch_results()
|
|
51
|
+
solver.print_branch_results(branch_results)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You can also run the bundled example directly:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
lfe-example
|
|
58
|
+
python -c "from load_flow_engine import run_example; run_example()"
|
|
59
|
+
python three_phase_loadflow.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Basic Workflow
|
|
63
|
+
|
|
64
|
+
The typical workflow is:
|
|
65
|
+
|
|
66
|
+
1. Create a `StudyCase`
|
|
67
|
+
2. Build a `Network`
|
|
68
|
+
3. Add buses, branches, transformers, and loads/generators
|
|
69
|
+
4. Call `solve()`
|
|
70
|
+
5. Read solved voltages from each `Bus` or call `compute_branch_results()`
|
|
71
|
+
|
|
72
|
+
Example skeleton:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import numpy as np
|
|
76
|
+
|
|
77
|
+
from load_flow_engine import (
|
|
78
|
+
Branch,
|
|
79
|
+
Bus,
|
|
80
|
+
BusType,
|
|
81
|
+
Load,
|
|
82
|
+
Network,
|
|
83
|
+
PhaseType,
|
|
84
|
+
StudyCase,
|
|
85
|
+
ThreePhaseLoadFlowSolver,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
sc = StudyCase(max_iterations=50, solution_precision=1e-4, base_mva=10.0)
|
|
89
|
+
net = Network(sc)
|
|
90
|
+
|
|
91
|
+
net.add_bus(Bus("SOURCE", BusType.SLACK, PhaseType.ABC, base_kv=12.47))
|
|
92
|
+
net.add_bus(Bus("LOADBUS", BusType.PQ, PhaseType.ABC, base_kv=12.47))
|
|
93
|
+
|
|
94
|
+
net.add_branch(Branch(
|
|
95
|
+
id="LINE1",
|
|
96
|
+
from_bus="SOURCE",
|
|
97
|
+
to_bus="LOADBUS",
|
|
98
|
+
phase_type=PhaseType.ABC,
|
|
99
|
+
r1=0.01,
|
|
100
|
+
x1=0.03,
|
|
101
|
+
r0=0.03,
|
|
102
|
+
x0=0.09,
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
net.add_load(Load(
|
|
106
|
+
id="LD1",
|
|
107
|
+
bus_id="LOADBUS",
|
|
108
|
+
phase_type=PhaseType.ABC,
|
|
109
|
+
mw=np.array([0.2, 0.2, 0.2]),
|
|
110
|
+
mvar=np.array([0.08, 0.08, 0.08]),
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
solver = ThreePhaseLoadFlowSolver(net)
|
|
114
|
+
solver.solve()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Public API
|
|
118
|
+
|
|
119
|
+
The top-level package re-exports the main classes and helpers:
|
|
120
|
+
|
|
121
|
+
- `StudyCase`
|
|
122
|
+
- `Bus`, `Branch`, `Transformer`, `Load`, `Generator`, `Shunt`
|
|
123
|
+
- `Network`
|
|
124
|
+
- `ThreePhaseLoadFlowSolver`
|
|
125
|
+
- `PhaseType` and `BusType`
|
|
126
|
+
- `build_example_network()` and `run_example()`
|
|
127
|
+
|
|
128
|
+
`three_phase_loadflow.py` remains available as a compatibility shim that re-exports the same public symbols.
|
|
129
|
+
|
|
130
|
+
## Tools And Adapters
|
|
131
|
+
|
|
132
|
+
- `load_flow_engine.tools.sqlite_adapter`: persist a network to SQLite with `export_network()` and load it back with `import_network()`
|
|
133
|
+
- `load_flow_engine.tools.opendss_adapter`: export a populated network to an OpenDSS script
|
|
134
|
+
- `load_flow_engine.tools.cim_adapter`: import CIM XML or export CIM/CGMES-style files
|
|
135
|
+
- `load_flow_engine.tools.cyme_adapter`: push a network into a CYME study through `cympy`
|
|
136
|
+
- `load_flow_engine.tools.diagnostics`: run validation checks with `run_diagnostics()`
|
|
137
|
+
- `load_flow_engine.tools.output`: extract bus results into a pandas DataFrame and summarize phase loading
|
|
138
|
+
- `load_flow_engine.tools.load_allocation`: allocate total demand across loads based on connected transformer capacity
|
|
139
|
+
- `load_flow_engine.tools.multiconductor_adapter`: convert from pandapower-style multiconductor models into an LFE `Network`
|
|
140
|
+
|
|
141
|
+
## Project Layout
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
load_flow_engine/
|
|
145
|
+
__init__.py
|
|
146
|
+
models.py
|
|
147
|
+
network.py
|
|
148
|
+
solver.py
|
|
149
|
+
example.py
|
|
150
|
+
tools/
|
|
151
|
+
diagnostics/
|
|
152
|
+
cim_adapter.py
|
|
153
|
+
cyme_adapter.py
|
|
154
|
+
opendss_adapter.py
|
|
155
|
+
sqlite_adapter.py
|
|
156
|
+
three_phase_loadflow.py
|
|
157
|
+
notebooks/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Notes
|
|
161
|
+
|
|
162
|
+
- Core runtime dependencies are `numpy` and `scipy`.
|
|
163
|
+
- The repository currently does not include an automated test suite.
|
|
164
|
+
- The example feeder in this repository solves successfully through the legacy script entrypoint.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
No license file is currently included in this repository.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
load_flow_engine — Three-phase unbalanced load flow solver package.
|
|
3
|
+
|
|
4
|
+
Re-exports all public symbols for convenience::
|
|
5
|
+
|
|
6
|
+
from load_flow_engine import Network, ThreePhaseLoadFlowSolver, Bus, Branch, ...
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .enums import PhaseType, BusType
|
|
10
|
+
from .constants import _a, _A, _Ai
|
|
11
|
+
from .models import (
|
|
12
|
+
StudyCase,
|
|
13
|
+
Bus,
|
|
14
|
+
Branch,
|
|
15
|
+
Transformer,
|
|
16
|
+
Load,
|
|
17
|
+
Generator,
|
|
18
|
+
Shunt,
|
|
19
|
+
BranchResult,
|
|
20
|
+
)
|
|
21
|
+
from .helpers import _active_phases, _seq_to_z_abc, _matrix_invert_3x3
|
|
22
|
+
from .network import Network
|
|
23
|
+
from .solver import ThreePhaseLoadFlowSolver
|
|
24
|
+
from .example import build_example_network, run_example
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Enums
|
|
28
|
+
"PhaseType", "BusType",
|
|
29
|
+
# Constants
|
|
30
|
+
"_a", "_A", "_Ai",
|
|
31
|
+
# Models
|
|
32
|
+
"StudyCase", "Bus", "Branch", "Transformer", "Load",
|
|
33
|
+
"Generator", "Shunt", "BranchResult",
|
|
34
|
+
# Helpers
|
|
35
|
+
"_active_phases", "_seq_to_z_abc", "_matrix_invert_3x3",
|
|
36
|
+
# Network
|
|
37
|
+
"Network",
|
|
38
|
+
# Solver
|
|
39
|
+
"ThreePhaseLoadFlowSolver",
|
|
40
|
+
# Example
|
|
41
|
+
"build_example_network", "run_example",
|
|
42
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symmetrical-component (Fortescue) transformation matrices.
|
|
3
|
+
Mirrors ETAP's use of A-matrix for zero/pos/neg sequence decomposition.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
_a = np.exp(1j * 2.0 * np.pi / 3.0) # 1∠120°
|
|
9
|
+
_A = np.array([[1, 1, 1 ], # Fortescue A
|
|
10
|
+
[1, _a**2, _a ],
|
|
11
|
+
[1, _a, _a**2 ]], dtype=complex)
|
|
12
|
+
_Ai = np.linalg.inv(_A) # A⁻¹
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Phase and bus type enumerations matching ETAP definitions."""
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PhaseType(IntEnum):
|
|
7
|
+
"""
|
|
8
|
+
Matches ETAPPhaseType enum in NetworkReductionEC.h.
|
|
9
|
+
Defines which phases a bus or branch participates in.
|
|
10
|
+
"""
|
|
11
|
+
ABC = 0 # three-phase
|
|
12
|
+
A = 5 # single-phase A
|
|
13
|
+
B = 6 # single-phase B
|
|
14
|
+
C = 7 # single-phase C
|
|
15
|
+
AB = 8 # two-phase A-B
|
|
16
|
+
BC = 9 # two-phase B-C
|
|
17
|
+
CA = 10 # two-phase C-A
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BusType(IntEnum):
|
|
21
|
+
"""Standard power-flow bus types."""
|
|
22
|
+
SLACK = 0 # swing bus — |V| and ∠V fixed per phase
|
|
23
|
+
PV = 1 # voltage-controlled — P and |V| fixed, Q within limits
|
|
24
|
+
PQ = 2 # load bus — P and Q fixed per phase
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Four-bus radial distribution feeder example.
|
|
3
|
+
|
|
4
|
+
Topology (12.47 kV primary, 4.16 kV secondary after transformer):
|
|
5
|
+
|
|
6
|
+
BUS1 (slack) ──── XFM1 ──── BUS2 ──── BR1 ──── BUS3 ──── BR2 ──── BUS4
|
|
7
|
+
12.47 kV 4.16 kV 4.16 kV 4.16 kV
|
|
8
|
+
swing bus 3φ balanced load 3φ+1φ unbalanced load 1φ-A load only
|
|
9
|
+
|
|
10
|
+
BUS2: 3-phase balanced load 150 kW + j50 kVAr on each phase
|
|
11
|
+
BUS3: unbalanced load 300 kW/ph-A, 100 kW/ph-B, 200 kW/ph-C + reactive
|
|
12
|
+
BUS4: single-phase A load 400 kW + j200 kVAr on phase A only
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .enums import PhaseType, BusType
|
|
18
|
+
from .constants import _Ai
|
|
19
|
+
from .models import Bus, Branch, Transformer, Load, StudyCase
|
|
20
|
+
from .network import Network
|
|
21
|
+
from .solver import ThreePhaseLoadFlowSolver
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_example_network() -> Network:
|
|
25
|
+
sc = StudyCase(max_iterations=50, solution_precision=1e-4, base_mva=10.0)
|
|
26
|
+
net = Network(sc)
|
|
27
|
+
|
|
28
|
+
# ---- Buses ----
|
|
29
|
+
net.add_bus(Bus("BUS1", BusType.SLACK, PhaseType.ABC, base_kv=12.47))
|
|
30
|
+
net.add_bus(Bus("BUS2", BusType.PQ, PhaseType.ABC, base_kv=4.16))
|
|
31
|
+
net.add_bus(Bus("BUS3", BusType.PQ, PhaseType.ABC, base_kv=4.16))
|
|
32
|
+
net.add_bus(Bus("BUS4", BusType.PQ, PhaseType.ABC, base_kv=4.16))
|
|
33
|
+
|
|
34
|
+
# ---- Transformer BUS1 → BUS2 ----
|
|
35
|
+
# 3 MVA, 12.47/4.16 kV, wye-grounded / wye-grounded
|
|
36
|
+
# Z1 = 1%, Z0 = 1% on transformer base
|
|
37
|
+
net.add_transformer(Transformer(
|
|
38
|
+
id="XFM1", from_bus="BUS1", to_bus="BUS2",
|
|
39
|
+
r1=0.005, x1=0.06, r0=0.005, x0=0.06,
|
|
40
|
+
mva_rating=3.0,
|
|
41
|
+
conn_primary='wye_grounded', conn_secondary='wye_grounded',
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
# ---- Branch BUS2 → BUS3 (3-phase overhead line, 1 km) ----
|
|
45
|
+
# Z1 = 0.306 + j0.627 Ω/km, Z0 = 0.745 + j1.2 Ω/km
|
|
46
|
+
# Base Z = (4.16²/10) = 1.731 Ω → divide to get pu
|
|
47
|
+
Zbase = 4.16**2 / 10.0 # 1.731 Ω
|
|
48
|
+
net.add_branch(Branch(
|
|
49
|
+
id="BR1", from_bus="BUS2", to_bus="BUS3",
|
|
50
|
+
phase_type=PhaseType.ABC,
|
|
51
|
+
r1=0.306/Zbase, x1=0.627/Zbase,
|
|
52
|
+
r0=0.745/Zbase, x0=1.200/Zbase,
|
|
53
|
+
b1=0.0,
|
|
54
|
+
ampacity=np.full(3, 300.0), # 300 A rated
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
# ---- Branch BUS3 → BUS4 (single-phase A, 0.5 km) ----
|
|
58
|
+
net.add_branch(Branch(
|
|
59
|
+
id="BR2", from_bus="BUS3", to_bus="BUS4",
|
|
60
|
+
phase_type=PhaseType.A,
|
|
61
|
+
r1=(0.306*0.5)/Zbase, x1=(0.627*0.5)/Zbase,
|
|
62
|
+
r0=(0.745*0.5)/Zbase, x0=(1.200*0.5)/Zbase,
|
|
63
|
+
ampacity=np.array([200.0, 0.0, 0.0]),
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
# ---- Loads ----
|
|
67
|
+
# BUS2: balanced 3-phase load 150 kW + j50 kVAr / phase
|
|
68
|
+
net.add_load(Load("LD2", "BUS2", PhaseType.ABC,
|
|
69
|
+
mw = np.array([0.15, 0.15, 0.15]),
|
|
70
|
+
mvar = np.array([0.05, 0.05, 0.05])))
|
|
71
|
+
|
|
72
|
+
# BUS3: unbalanced 3-phase load
|
|
73
|
+
net.add_load(Load("LD3", "BUS3", PhaseType.ABC,
|
|
74
|
+
mw = np.array([0.30, 0.10, 0.20]),
|
|
75
|
+
mvar = np.array([0.15, 0.05, 0.10])))
|
|
76
|
+
|
|
77
|
+
# BUS4: single-phase A load 400 kW + j200 kVAr
|
|
78
|
+
net.add_load(Load("LD4", "BUS4", PhaseType.A,
|
|
79
|
+
mw = np.array([0.40, 0.0, 0.0]),
|
|
80
|
+
mvar = np.array([0.20, 0.0, 0.0])))
|
|
81
|
+
|
|
82
|
+
return net
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_example() -> None:
|
|
86
|
+
print("\n" + "=" * 72)
|
|
87
|
+
print(" ETAP-Style Three-Phase Unbalanced Load Flow — 4-Bus Feeder")
|
|
88
|
+
print("=" * 72)
|
|
89
|
+
|
|
90
|
+
net = build_example_network()
|
|
91
|
+
solver = ThreePhaseLoadFlowSolver(net)
|
|
92
|
+
|
|
93
|
+
converged = solver.solve()
|
|
94
|
+
|
|
95
|
+
if not converged:
|
|
96
|
+
print("WARNING: solver did not converge!")
|
|
97
|
+
|
|
98
|
+
solver.print_bus_results()
|
|
99
|
+
|
|
100
|
+
br_results = solver.compute_branch_results()
|
|
101
|
+
solver.print_branch_results(br_results)
|
|
102
|
+
|
|
103
|
+
# ---- Voltage unbalance at BUS3 ----
|
|
104
|
+
b3 = net.buses["BUS3"]
|
|
105
|
+
V_abc = np.array([
|
|
106
|
+
b3.v_mag[p] * np.exp(1j * np.deg2rad(b3.v_ang[p]))
|
|
107
|
+
for p in range(3)
|
|
108
|
+
])
|
|
109
|
+
V_012 = _Ai @ V_abc
|
|
110
|
+
vuf_neg = abs(V_012[2]) / abs(V_012[1]) * 100.0
|
|
111
|
+
vuf_zero = abs(V_012[0]) / abs(V_012[1]) * 100.0
|
|
112
|
+
print(f" BUS3 Voltage Unbalance Factor (NEMA neg-seq) : {vuf_neg:.3f} %")
|
|
113
|
+
print(f" BUS3 Voltage Unbalance Factor (zero-seq) : {vuf_zero:.3f} %")
|
|
114
|
+
print()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Helper functions for impedance transforms and phase mapping."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .enums import PhaseType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _active_phases(phase_type: PhaseType) -> List[int]:
|
|
11
|
+
"""
|
|
12
|
+
Return the sorted list of active phase indices (0=A, 1=B, 2=C)
|
|
13
|
+
for a given PhaseType, matching ETAPPhaseType handling in
|
|
14
|
+
NetworkReductionEC.cpp.
|
|
15
|
+
"""
|
|
16
|
+
mapping = {
|
|
17
|
+
PhaseType.ABC: [0, 1, 2],
|
|
18
|
+
PhaseType.A: [0],
|
|
19
|
+
PhaseType.B: [1],
|
|
20
|
+
PhaseType.C: [2],
|
|
21
|
+
PhaseType.AB: [0, 1],
|
|
22
|
+
PhaseType.BC: [1, 2],
|
|
23
|
+
PhaseType.CA: [0, 2],
|
|
24
|
+
}
|
|
25
|
+
return sorted(mapping.get(phase_type, [0, 1, 2]))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _seq_to_z_abc(z1: complex, z0: complex) -> np.ndarray:
|
|
29
|
+
"""
|
|
30
|
+
Convert positive- and zero-sequence impedances to the 3×3 phase-domain
|
|
31
|
+
impedance matrix for a fully transposed line.
|
|
32
|
+
|
|
33
|
+
Mirrors NetworkReductionEC's use of the symmetrical-component transform:
|
|
34
|
+
Z_abc = A · diag(Z0, Z1, Z1) · A⁻¹
|
|
35
|
+
|
|
36
|
+
For a transposed line this reduces to the Toeplitz form:
|
|
37
|
+
Zs = (Z0 + 2·Z1) / 3 (self impedance)
|
|
38
|
+
Zm = (Z0 − Z1) / 3 (mutual impedance)
|
|
39
|
+
|
|
40
|
+
Z_abc = [[Zs, Zm, Zm],
|
|
41
|
+
[Zm, Zs, Zm],
|
|
42
|
+
[Zm, Zm, Zs]]
|
|
43
|
+
"""
|
|
44
|
+
Zs = (z0 + 2.0 * z1) / 3.0
|
|
45
|
+
Zm = (z0 - z1) / 3.0
|
|
46
|
+
return np.array([[Zs, Zm, Zm],
|
|
47
|
+
[Zm, Zs, Zm],
|
|
48
|
+
[Zm, Zm, Zs]], dtype=complex)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _matrix_invert_3x3(Z: np.ndarray) -> Optional[np.ndarray]:
|
|
52
|
+
"""
|
|
53
|
+
Invert a 3×3 complex matrix using Gauss-Jordan elimination.
|
|
54
|
+
Mirrors MatrixInvert(DComplexEC mtxZ[3][3]) in NetworkReductionEC.cpp,
|
|
55
|
+
including the same singular-matrix guard (returns None on failure).
|
|
56
|
+
"""
|
|
57
|
+
SMALL = 1e-12
|
|
58
|
+
A = Z.astype(complex).copy()
|
|
59
|
+
I = np.eye(3, dtype=complex)
|
|
60
|
+
|
|
61
|
+
for i in range(3):
|
|
62
|
+
# Partial pivoting (mirrors ETAP's row-sum fallback)
|
|
63
|
+
if abs(A[i, i]) < SMALL:
|
|
64
|
+
swapped = False
|
|
65
|
+
for k in range(i + 1, 3):
|
|
66
|
+
if abs(A[k, i]) >= SMALL:
|
|
67
|
+
A[[i, k]] = A[[k, i]]
|
|
68
|
+
I[[i, k]] = I[[k, i]]
|
|
69
|
+
swapped = True
|
|
70
|
+
break
|
|
71
|
+
if not swapped:
|
|
72
|
+
return None # singular
|
|
73
|
+
|
|
74
|
+
pivot = A[i, i]
|
|
75
|
+
A[i] /= pivot
|
|
76
|
+
I[i] /= pivot
|
|
77
|
+
|
|
78
|
+
for k in range(3):
|
|
79
|
+
if k != i and abs(A[k, i]) >= SMALL:
|
|
80
|
+
factor = A[k, i]
|
|
81
|
+
A[k] -= factor * A[i]
|
|
82
|
+
I[k] -= factor * I[i]
|
|
83
|
+
|
|
84
|
+
return I
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Data classes mirroring ETAP DB accessor structures."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .enums import PhaseType, BusType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class StudyCase:
|
|
13
|
+
"""
|
|
14
|
+
Mirrors LF3PH_STUDY_CASE_DATA from dataTyp6.h.
|
|
15
|
+
Controls solver parameters.
|
|
16
|
+
"""
|
|
17
|
+
max_iterations: int = 100 # maximumIteration
|
|
18
|
+
solution_precision: float = 1e-4 # solutionPrecision (MVA mismatch)
|
|
19
|
+
base_mva: float = 100.0 # system MVA base
|
|
20
|
+
flat_start: bool = True # use 1.0 pu / nominal angles
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Bus:
|
|
25
|
+
"""
|
|
26
|
+
Mirrors IBusLF3PHAccessor from IBusLF3PH.h.
|
|
27
|
+
|
|
28
|
+
All voltage magnitudes are in per-unit; angles in degrees.
|
|
29
|
+
Powers are in MW and MVAr (converted to pu internally during solve).
|
|
30
|
+
Array index 0=A, 1=B, 2=C throughout.
|
|
31
|
+
"""
|
|
32
|
+
id: str
|
|
33
|
+
bus_type: BusType = BusType.PQ
|
|
34
|
+
phase_type: PhaseType = PhaseType.ABC
|
|
35
|
+
base_kv: float = 12.47 # m_BasekV
|
|
36
|
+
name: str = '' # MC source name for cross-referencing
|
|
37
|
+
|
|
38
|
+
# Solved voltages — m_VMagA/B/C, m_VAngA/B/C
|
|
39
|
+
v_mag: np.ndarray = field(
|
|
40
|
+
default_factory=lambda: np.ones(3))
|
|
41
|
+
v_ang: np.ndarray = field(
|
|
42
|
+
default_factory=lambda: np.array([0.0, -120.0, 120.0]))
|
|
43
|
+
|
|
44
|
+
# Initial voltage guess — m_IniVMagA/B/C, m_IniAngA/B/C
|
|
45
|
+
ini_v_mag: np.ndarray = field(
|
|
46
|
+
default_factory=lambda: np.ones(3))
|
|
47
|
+
ini_v_ang: np.ndarray = field(
|
|
48
|
+
default_factory=lambda: np.array([0.0, -120.0, 120.0]))
|
|
49
|
+
|
|
50
|
+
# Scheduled generation per phase (MW, MVAr) — m_GenMWA/B/C, m_GenMvarA/B/C
|
|
51
|
+
gen_mw: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
52
|
+
gen_mvar: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
53
|
+
|
|
54
|
+
# Scheduled load per phase (MW, MVAr) — m_LoadMWA/B/C, m_LoadMvarA/B/C
|
|
55
|
+
load_mw: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
56
|
+
load_mvar: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
57
|
+
|
|
58
|
+
# Reactive limits (PV buses) — m_MvarMax, m_MvarMin
|
|
59
|
+
mvar_max: float = 9999.0
|
|
60
|
+
mvar_min: float = -9999.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Branch:
|
|
65
|
+
"""
|
|
66
|
+
Mirrors IConnectLF3PHAccessor from IConnectLF3PH.h.
|
|
67
|
+
|
|
68
|
+
Stores positive- and zero-sequence impedances in per-unit.
|
|
69
|
+
The 3×3 phase-domain matrix Z_abc is computed at build time via the
|
|
70
|
+
symmetrical-component transform (same as NetworkReductionEC).
|
|
71
|
+
"""
|
|
72
|
+
id: str
|
|
73
|
+
from_bus: str
|
|
74
|
+
to_bus: str
|
|
75
|
+
phase_type: PhaseType = PhaseType.ABC
|
|
76
|
+
name: str = ''
|
|
77
|
+
|
|
78
|
+
# Positive-sequence series impedance (pu) — m_R, m_X
|
|
79
|
+
r1: float = 0.0
|
|
80
|
+
x1: float = 0.0
|
|
81
|
+
|
|
82
|
+
# Zero-sequence series impedance (pu) — m_R0, m_X0
|
|
83
|
+
r0: float = 0.0
|
|
84
|
+
x0: float = 0.0
|
|
85
|
+
|
|
86
|
+
# Shunt charging susceptance (pu, positive-sequence total line charging)
|
|
87
|
+
b1: float = 0.0
|
|
88
|
+
|
|
89
|
+
# Ampacity per phase (amps) for loading-% calc — m_AmpacityA/B/C
|
|
90
|
+
ampacity: np.ndarray = field(default_factory=lambda: np.full(3, 9999.0))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Transformer:
|
|
95
|
+
"""
|
|
96
|
+
Two-winding transformer.
|
|
97
|
+
Mirrors LF3PH_Xfmr2Data from dataTyp6.h.
|
|
98
|
+
|
|
99
|
+
Impedances are in pu on the transformer's own MVA base; they are
|
|
100
|
+
converted to the system base during Network.build().
|
|
101
|
+
"""
|
|
102
|
+
id: str
|
|
103
|
+
from_bus: str # primary (HV) bus
|
|
104
|
+
to_bus: str # secondary (LV) bus
|
|
105
|
+
phase_type: PhaseType = PhaseType.ABC
|
|
106
|
+
name: str = ''
|
|
107
|
+
|
|
108
|
+
# Leakage impedance (pu on xfmr base) — positive-sequence
|
|
109
|
+
r1: float = 0.0
|
|
110
|
+
x1: float = 0.01
|
|
111
|
+
|
|
112
|
+
# Zero-sequence leakage impedance
|
|
113
|
+
r0: float = 0.0
|
|
114
|
+
x0: float = 0.01
|
|
115
|
+
|
|
116
|
+
# Transformer MVA rating (used for base conversion)
|
|
117
|
+
mva_rating: float = 1.0
|
|
118
|
+
|
|
119
|
+
# Off-nominal tap (pu of nominal ratio) — m_TapPctPrim / m_TapPctSec
|
|
120
|
+
tap_primary: float = 1.0
|
|
121
|
+
tap_secondary: float = 1.0
|
|
122
|
+
|
|
123
|
+
# Winding connections (affects zero-sequence path)
|
|
124
|
+
# Supported: 'wye_grounded', 'wye', 'delta'
|
|
125
|
+
conn_primary: str = 'wye_grounded'
|
|
126
|
+
conn_secondary: str = 'wye_grounded'
|
|
127
|
+
|
|
128
|
+
# HV phases this transformer is connected to (0=A, 1=B, 2=C)
|
|
129
|
+
hv_phases: List[int] = field(default_factory=lambda: [0, 1, 2])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class Load:
|
|
134
|
+
"""
|
|
135
|
+
Static load element — mirrors LF3PH_StaticLoadData from dataTyp6.h.
|
|
136
|
+
Per-phase MW and MVAr. add_load() aggregates these onto the bus.
|
|
137
|
+
"""
|
|
138
|
+
id: str
|
|
139
|
+
bus_id: str
|
|
140
|
+
phase_type: PhaseType = PhaseType.ABC
|
|
141
|
+
name: str = ''
|
|
142
|
+
mw: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
143
|
+
mvar: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class Generator:
|
|
148
|
+
"""
|
|
149
|
+
Synchronous generator — mirrors LF3PH_SynGenData from dataTyp6.h.
|
|
150
|
+
"""
|
|
151
|
+
id: str
|
|
152
|
+
bus_id: str
|
|
153
|
+
bus_type: BusType = BusType.PV
|
|
154
|
+
name: str = ''
|
|
155
|
+
|
|
156
|
+
mw: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
157
|
+
v_set_pu: float = 1.0
|
|
158
|
+
mvar_max: float = 9999.0
|
|
159
|
+
mvar_min: float = -9999.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Shunt:
|
|
164
|
+
"""
|
|
165
|
+
Shunt element — per-phase MW and MVAr (capacitor/reactor banks).
|
|
166
|
+
"""
|
|
167
|
+
id: str
|
|
168
|
+
bus_id: str
|
|
169
|
+
phase_type: PhaseType = PhaseType.ABC
|
|
170
|
+
name: str = ''
|
|
171
|
+
p_mw: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
172
|
+
q_mvar: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
173
|
+
vn_kv: float = 12.47
|
|
174
|
+
closed: bool = True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Switch:
|
|
179
|
+
"""
|
|
180
|
+
Switch element — connects two buses with optional resistance.
|
|
181
|
+
"""
|
|
182
|
+
id: str
|
|
183
|
+
bus: int = 0
|
|
184
|
+
element: int = 0
|
|
185
|
+
et: str = 'b' # 'b' = bus-bus, 'l' = line
|
|
186
|
+
sw_type: str = 'LBS'
|
|
187
|
+
closed: bool = True
|
|
188
|
+
phase: int = 0
|
|
189
|
+
r_ohm: float = 0.0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class BranchResult:
|
|
194
|
+
"""
|
|
195
|
+
Post-solve per-branch results.
|
|
196
|
+
Field names mirror LFSumBranchLF3PHAccessor from LFSumBranchLF3PH.h.
|
|
197
|
+
"""
|
|
198
|
+
id: str
|
|
199
|
+
from_bus: str
|
|
200
|
+
to_bus: str
|
|
201
|
+
|
|
202
|
+
# Per-phase current — m_LoadingAmpMagA/B/C, m_LoadingAmpAngA/B/C
|
|
203
|
+
i_mag_abc: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
204
|
+
i_ang_abc: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
205
|
+
|
|
206
|
+
# Per-phase apparent power (MVA) from/to — m_LoadingInMVAA/B/C, m_LoadingOutMVAA/B/C
|
|
207
|
+
mva_from: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
208
|
+
mva_to: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
209
|
+
|
|
210
|
+
# Per-phase loading percent — m_Loading_A/B/C
|
|
211
|
+
loading_pct: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
212
|
+
|
|
213
|
+
# Sequence currents — m_LoadingAmpMag0/1/2, m_LoadingAmpAng0/1/2
|
|
214
|
+
i_mag_012: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
215
|
+
i_ang_012: np.ndarray = field(default_factory=lambda: np.zeros(3))
|
|
216
|
+
|
|
217
|
+
# Unbalance factors — m_CUF2 (negative-seq), m_CUF0 (zero-seq)
|
|
218
|
+
cuf2: float = 0.0
|
|
219
|
+
cuf0: float = 0.0
|