4pace 0.1.0a0__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.
- 4pace-0.1.0a0/LICENSE +21 -0
- 4pace-0.1.0a0/PKG-INFO +59 -0
- 4pace-0.1.0a0/README.md +40 -0
- 4pace-0.1.0a0/pyproject.toml +28 -0
- 4pace-0.1.0a0/setup.cfg +4 -0
- 4pace-0.1.0a0/src/4pace.egg-info/PKG-INFO +59 -0
- 4pace-0.1.0a0/src/4pace.egg-info/SOURCES.txt +11 -0
- 4pace-0.1.0a0/src/4pace.egg-info/dependency_links.txt +1 -0
- 4pace-0.1.0a0/src/4pace.egg-info/requires.txt +4 -0
- 4pace-0.1.0a0/src/4pace.egg-info/top_level.txt +1 -0
- 4pace-0.1.0a0/src/fourpace/__init__.py +0 -0
- 4pace-0.1.0a0/src/fourpace/model.py +156 -0
- 4pace-0.1.0a0/src/fourpace/psys.py +482 -0
4pace-0.1.0a0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 daniel-sakdinun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
4pace-0.1.0a0/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 4pace
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: Four-Quadrant Power Analysis & Computational Engine
|
|
5
|
+
Author-email: daniel-sakdinun <sakdinun.thewana@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/daniel-sakdinun/4PACE.git
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: networkx
|
|
17
|
+
Requires-Dist: pyyaml
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ⚡ Four-Quadrant Power Analysis & Computational Engine (4PACE)
|
|
21
|
+
|
|
22
|
+
[]()
|
|
23
|
+
[](https://www.python.org/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
> **⚠️ Early Access Notice**
|
|
27
|
+
>
|
|
28
|
+
> This project is currently in **Early Access (Pre-v1.0.0)**. While the core engine is functional and capable of solving complex power flow and optimization problems, it is still undergoing significant development. Features may change, and bug reports or feedback are highly encouraged to help reach the stable v1.0.0 release.
|
|
29
|
+
|
|
30
|
+
4PACE is a high-precision Power System Optimization Engine designed for analyzing and determining the most efficient operating points for electrical grids. Developed entirely in Python using only core scientific libraries, it ensures maximum computational efficiency and provides the flexibility needed to scale capabilities for modern grid demands.
|
|
31
|
+
|
|
32
|
+
## 🧐 Why "4PACE"?
|
|
33
|
+
|
|
34
|
+
The name 4PACE reflects the core pillars of this project:
|
|
35
|
+
* **4-Quadrant:** The ability to simulate electrical equipment behavior across all four quadrants of active and reactive power, whether operating as a Generator, Motor, or Condenser.
|
|
36
|
+
* **Power Analysis:** Comprehensive steady-state analysis powered by a robust Newton-Raphson solver.
|
|
37
|
+
* **Computational** Engine: Driven by the mathematical prowess of NumPy and SciPy to ensure rapid numerical convergence.
|
|
38
|
+
|
|
39
|
+
## ✨ Key Features
|
|
40
|
+
|
|
41
|
+
* **Network Topology Management:** Built on top of `networkx` for robust and intuitive graph-based grid modeling.
|
|
42
|
+
* **Steady-State Analysis:** Accurate **Newton-Raphson Power Flow** algorithm for evaluating grid voltage and power distribution.
|
|
43
|
+
* **Optimal Power Flow (OPF):**
|
|
44
|
+
* **AC OPF:** Full non-linear optimization considering $I^2R$ losses, voltage limits, reactive power (Q), and branch flow limits ($S_{max}$).
|
|
45
|
+
* **Economic Dispatch:** Lambda iteration method for minimizing generation fuel costs.
|
|
46
|
+
* **Advanced Equipment Models:**
|
|
47
|
+
* **Synchronous Machines:** Supports both Generator and Motor modes with PQ limits.
|
|
48
|
+
* **Branch Models:** Pi-model Transmission Lines and Off-nominal Tap / Phase-shifting Transformers.
|
|
49
|
+
* **Loads:** Supports Constant Power (PQ), Constant Current (I), and Constant Impedance (Z) models.
|
|
50
|
+
* **Human-Readable Config:** Effortlessly load entire grid topologies via `YAML` without hardcoding Python scripts.
|
|
51
|
+
|
|
52
|
+
## 📦 Installation
|
|
53
|
+
|
|
54
|
+
Clone this repository and install the required dependencies. This engine is intentionally kept lightweight and relies only on core scientific libraries.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/yourusername/4PACE.git
|
|
58
|
+
cd 4PACE
|
|
59
|
+
pip install -r requirements.txt
|
4pace-0.1.0a0/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ⚡ Four-Quadrant Power Analysis & Computational Engine (4PACE)
|
|
2
|
+
|
|
3
|
+
[]()
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> **⚠️ Early Access Notice**
|
|
8
|
+
>
|
|
9
|
+
> This project is currently in **Early Access (Pre-v1.0.0)**. While the core engine is functional and capable of solving complex power flow and optimization problems, it is still undergoing significant development. Features may change, and bug reports or feedback are highly encouraged to help reach the stable v1.0.0 release.
|
|
10
|
+
|
|
11
|
+
4PACE is a high-precision Power System Optimization Engine designed for analyzing and determining the most efficient operating points for electrical grids. Developed entirely in Python using only core scientific libraries, it ensures maximum computational efficiency and provides the flexibility needed to scale capabilities for modern grid demands.
|
|
12
|
+
|
|
13
|
+
## 🧐 Why "4PACE"?
|
|
14
|
+
|
|
15
|
+
The name 4PACE reflects the core pillars of this project:
|
|
16
|
+
* **4-Quadrant:** The ability to simulate electrical equipment behavior across all four quadrants of active and reactive power, whether operating as a Generator, Motor, or Condenser.
|
|
17
|
+
* **Power Analysis:** Comprehensive steady-state analysis powered by a robust Newton-Raphson solver.
|
|
18
|
+
* **Computational** Engine: Driven by the mathematical prowess of NumPy and SciPy to ensure rapid numerical convergence.
|
|
19
|
+
|
|
20
|
+
## ✨ Key Features
|
|
21
|
+
|
|
22
|
+
* **Network Topology Management:** Built on top of `networkx` for robust and intuitive graph-based grid modeling.
|
|
23
|
+
* **Steady-State Analysis:** Accurate **Newton-Raphson Power Flow** algorithm for evaluating grid voltage and power distribution.
|
|
24
|
+
* **Optimal Power Flow (OPF):**
|
|
25
|
+
* **AC OPF:** Full non-linear optimization considering $I^2R$ losses, voltage limits, reactive power (Q), and branch flow limits ($S_{max}$).
|
|
26
|
+
* **Economic Dispatch:** Lambda iteration method for minimizing generation fuel costs.
|
|
27
|
+
* **Advanced Equipment Models:**
|
|
28
|
+
* **Synchronous Machines:** Supports both Generator and Motor modes with PQ limits.
|
|
29
|
+
* **Branch Models:** Pi-model Transmission Lines and Off-nominal Tap / Phase-shifting Transformers.
|
|
30
|
+
* **Loads:** Supports Constant Power (PQ), Constant Current (I), and Constant Impedance (Z) models.
|
|
31
|
+
* **Human-Readable Config:** Effortlessly load entire grid topologies via `YAML` without hardcoding Python scripts.
|
|
32
|
+
|
|
33
|
+
## 📦 Installation
|
|
34
|
+
|
|
35
|
+
Clone this repository and install the required dependencies. This engine is intentionally kept lightweight and relies only on core scientific libraries.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/yourusername/4PACE.git
|
|
39
|
+
cd 4PACE
|
|
40
|
+
pip install -r requirements.txt
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "4pace"
|
|
7
|
+
version = "0.1.0-alpha"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="daniel-sakdinun", email="sakdinun.thewana@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Four-Quadrant Power Analysis & Computational Engine"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy",
|
|
22
|
+
"scipy",
|
|
23
|
+
"networkx",
|
|
24
|
+
"pyyaml",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
"Homepage" = "https://github.com/daniel-sakdinun/4PACE.git"
|
4pace-0.1.0a0/setup.cfg
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 4pace
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: Four-Quadrant Power Analysis & Computational Engine
|
|
5
|
+
Author-email: daniel-sakdinun <sakdinun.thewana@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/daniel-sakdinun/4PACE.git
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: networkx
|
|
17
|
+
Requires-Dist: pyyaml
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ⚡ Four-Quadrant Power Analysis & Computational Engine (4PACE)
|
|
21
|
+
|
|
22
|
+
[]()
|
|
23
|
+
[](https://www.python.org/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
> **⚠️ Early Access Notice**
|
|
27
|
+
>
|
|
28
|
+
> This project is currently in **Early Access (Pre-v1.0.0)**. While the core engine is functional and capable of solving complex power flow and optimization problems, it is still undergoing significant development. Features may change, and bug reports or feedback are highly encouraged to help reach the stable v1.0.0 release.
|
|
29
|
+
|
|
30
|
+
4PACE is a high-precision Power System Optimization Engine designed for analyzing and determining the most efficient operating points for electrical grids. Developed entirely in Python using only core scientific libraries, it ensures maximum computational efficiency and provides the flexibility needed to scale capabilities for modern grid demands.
|
|
31
|
+
|
|
32
|
+
## 🧐 Why "4PACE"?
|
|
33
|
+
|
|
34
|
+
The name 4PACE reflects the core pillars of this project:
|
|
35
|
+
* **4-Quadrant:** The ability to simulate electrical equipment behavior across all four quadrants of active and reactive power, whether operating as a Generator, Motor, or Condenser.
|
|
36
|
+
* **Power Analysis:** Comprehensive steady-state analysis powered by a robust Newton-Raphson solver.
|
|
37
|
+
* **Computational** Engine: Driven by the mathematical prowess of NumPy and SciPy to ensure rapid numerical convergence.
|
|
38
|
+
|
|
39
|
+
## ✨ Key Features
|
|
40
|
+
|
|
41
|
+
* **Network Topology Management:** Built on top of `networkx` for robust and intuitive graph-based grid modeling.
|
|
42
|
+
* **Steady-State Analysis:** Accurate **Newton-Raphson Power Flow** algorithm for evaluating grid voltage and power distribution.
|
|
43
|
+
* **Optimal Power Flow (OPF):**
|
|
44
|
+
* **AC OPF:** Full non-linear optimization considering $I^2R$ losses, voltage limits, reactive power (Q), and branch flow limits ($S_{max}$).
|
|
45
|
+
* **Economic Dispatch:** Lambda iteration method for minimizing generation fuel costs.
|
|
46
|
+
* **Advanced Equipment Models:**
|
|
47
|
+
* **Synchronous Machines:** Supports both Generator and Motor modes with PQ limits.
|
|
48
|
+
* **Branch Models:** Pi-model Transmission Lines and Off-nominal Tap / Phase-shifting Transformers.
|
|
49
|
+
* **Loads:** Supports Constant Power (PQ), Constant Current (I), and Constant Impedance (Z) models.
|
|
50
|
+
* **Human-Readable Config:** Effortlessly load entire grid topologies via `YAML` without hardcoding Python scripts.
|
|
51
|
+
|
|
52
|
+
## 📦 Installation
|
|
53
|
+
|
|
54
|
+
Clone this repository and install the required dependencies. This engine is intentionally kept lightweight and relies only on core scientific libraries.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/yourusername/4PACE.git
|
|
58
|
+
cd 4PACE
|
|
59
|
+
pip install -r requirements.txt
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/4pace.egg-info/PKG-INFO
|
|
5
|
+
src/4pace.egg-info/SOURCES.txt
|
|
6
|
+
src/4pace.egg-info/dependency_links.txt
|
|
7
|
+
src/4pace.egg-info/requires.txt
|
|
8
|
+
src/4pace.egg-info/top_level.txt
|
|
9
|
+
src/fourpace/__init__.py
|
|
10
|
+
src/fourpace/model.py
|
|
11
|
+
src/fourpace/psys.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fourpace
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class BusComponent(ABC):
|
|
5
|
+
def __init__(self, name: str, P: float = 0.0, Q: float = 0.0, R: float = 0.0, X: float = 0.0):
|
|
6
|
+
self.name = name
|
|
7
|
+
self.P = P
|
|
8
|
+
self.Q = Q
|
|
9
|
+
|
|
10
|
+
self.Z = complex(R, X)
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def S(self) -> complex:
|
|
14
|
+
"""Complex Power (S)"""
|
|
15
|
+
return complex(self.P, self.Q)
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def cost(self) -> float:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def incremental_cost(self) -> float:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class BranchComponent(ABC):
|
|
26
|
+
def __init__(self, name: str, R: float, X: float, S_max: float | None = None):
|
|
27
|
+
self.name = name
|
|
28
|
+
self.R = R
|
|
29
|
+
self.X = X
|
|
30
|
+
self.S_max = S_max
|
|
31
|
+
|
|
32
|
+
self.Z = complex(R, X)
|
|
33
|
+
self.Y = 1 / self.Z if self.Z != 0 else 0j
|
|
34
|
+
|
|
35
|
+
class SynchronousMachine(BusComponent):
|
|
36
|
+
def __init__(self, name: str, P: float = 0.0, Q: float = 0.0,
|
|
37
|
+
a: float = 0.0, b: float = 0.0, c: float= 0.0,
|
|
38
|
+
Pmin: float = 0.0, Pmax: float | None = None,
|
|
39
|
+
Qmin: float | None = None, Qmax: float | None = None,
|
|
40
|
+
S_rated: float | None = None, pf: float = 0.85, mode: str = 'generator',
|
|
41
|
+
R: float = 0.0, X: float = 0.0):
|
|
42
|
+
super().__init__(name, P, Q, R, X)
|
|
43
|
+
self.a, self.b, self.c = a, b, c
|
|
44
|
+
self.mode:str = mode
|
|
45
|
+
|
|
46
|
+
if S_rated is not None:
|
|
47
|
+
max_p = S_rated * pf
|
|
48
|
+
max_q = S_rated * np.sin(np.acos(pf))
|
|
49
|
+
|
|
50
|
+
if mode == 'generator':
|
|
51
|
+
self.Pmax = max_p
|
|
52
|
+
self.Pmin = Pmin if Pmin is not None else 0.0
|
|
53
|
+
elif mode == 'motor':
|
|
54
|
+
self.Pmax = Pmax if Pmax is not None else 0.0
|
|
55
|
+
self.Pmin = -max_p
|
|
56
|
+
elif mode == 'condenser':
|
|
57
|
+
self.Pmax = 0.0
|
|
58
|
+
self.Pmin = 0.0
|
|
59
|
+
elif mode == 'pumped_storage': # เป็นได้ทั้งสองอย่าง
|
|
60
|
+
self.Pmax = max_p
|
|
61
|
+
self.Pmin = -max_p
|
|
62
|
+
|
|
63
|
+
self.Qmax = max_q
|
|
64
|
+
self.Qmin = Qmin if Qmin is not None else (-max_q * 0.3)
|
|
65
|
+
else:
|
|
66
|
+
self.Pmax = Pmax if Pmax is not None else float('inf')
|
|
67
|
+
self.Pmin = Pmin if Pmin is not None else (0.0 if mode == 'generator' else float('-inf'))
|
|
68
|
+
self.Qmax = Qmax if Qmax is not None else float('inf')
|
|
69
|
+
self.Qmin = Qmin if Qmin is not None else float('-inf')
|
|
70
|
+
|
|
71
|
+
def cost(self) -> float:
|
|
72
|
+
if self.P <= 0:
|
|
73
|
+
return 0.0
|
|
74
|
+
return self.a + self.b*self.P + self.c*(self.P**2)
|
|
75
|
+
|
|
76
|
+
def incremental_cost(self) -> float:
|
|
77
|
+
if self.P <= 0:
|
|
78
|
+
return 0.0
|
|
79
|
+
return self.b + (2 * self.c * self.P)
|
|
80
|
+
|
|
81
|
+
class Load(BusComponent):
|
|
82
|
+
def __init__(self, name: str, model: str ='P',
|
|
83
|
+
P: float = 0, Q: float = 0,
|
|
84
|
+
R: float = 0, X: float = 0):
|
|
85
|
+
"""
|
|
86
|
+
model: 'Z' (constant Z, P ∝ V^2)
|
|
87
|
+
'I' (constant I, P ∝ V),
|
|
88
|
+
'P' (constant PQ),
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
super().__init__(name, -abs(P), -Q, R, X)
|
|
92
|
+
self.model = model
|
|
93
|
+
|
|
94
|
+
self.P_nom = self.P
|
|
95
|
+
self.Q_nom = self.Q
|
|
96
|
+
|
|
97
|
+
def cost(self) -> float:
|
|
98
|
+
return 0.0
|
|
99
|
+
|
|
100
|
+
def incremental_cost(self) -> float:
|
|
101
|
+
return 0.0
|
|
102
|
+
|
|
103
|
+
def update_voltage_dependence(self, V_mag: float, V_nom: float = 1.0):
|
|
104
|
+
if self.model == 'Z':
|
|
105
|
+
self.P = self.P_nom * (V_mag / V_nom)**2
|
|
106
|
+
self.Q = self.Q_nom * (V_mag / V_nom)**2
|
|
107
|
+
elif self.model == 'I':
|
|
108
|
+
self.P = self.P_nom * (V_mag / V_nom)
|
|
109
|
+
self.Q = self.Q_nom * (V_mag / V_nom)
|
|
110
|
+
elif self.model == 'P':
|
|
111
|
+
self.P = self.P_nom
|
|
112
|
+
self.Q = self.Q_nom
|
|
113
|
+
|
|
114
|
+
class Inverter(BusComponent):
|
|
115
|
+
def __init__(self, name: str, S_max: float,
|
|
116
|
+
P: float = 0.0, Q: float = 0.0,
|
|
117
|
+
control_mode: str = 'grid_following',
|
|
118
|
+
source_type: str = 'solar',
|
|
119
|
+
R: float = 0.0, X: float = 0.0):
|
|
120
|
+
super().__init__(name, P, Q, R, X)
|
|
121
|
+
self.S_max = S_max
|
|
122
|
+
self.control_mode = control_mode
|
|
123
|
+
self.source_type = source_type
|
|
124
|
+
|
|
125
|
+
self.Qmax = S_max
|
|
126
|
+
self.Qmin = -S_max
|
|
127
|
+
|
|
128
|
+
if source_type in ['solar', 'wind']:
|
|
129
|
+
self.Pmax = S_max
|
|
130
|
+
self.Pmin = 0.0
|
|
131
|
+
elif source_type == 'bess':
|
|
132
|
+
self.Pmax = S_max
|
|
133
|
+
self.Pmin = -S_max
|
|
134
|
+
|
|
135
|
+
def cost(self) -> float:
|
|
136
|
+
return 0.0
|
|
137
|
+
|
|
138
|
+
def incremental_cost(self) -> float:
|
|
139
|
+
return 0.0
|
|
140
|
+
|
|
141
|
+
class TransmissionLine(BranchComponent):
|
|
142
|
+
def __init__(self, name: str,
|
|
143
|
+
R: float, X: float, B_shunt:float = 0.0,
|
|
144
|
+
S_max: float | None = None, length_km: float = 1.0):
|
|
145
|
+
super().__init__(name, R, X, S_max)
|
|
146
|
+
self.B_shunt = B_shunt
|
|
147
|
+
self.length_km = length_km
|
|
148
|
+
|
|
149
|
+
class Transformer(BranchComponent):
|
|
150
|
+
def __init__(self, name: str,
|
|
151
|
+
R: float, X: float,
|
|
152
|
+
tap_ratio: float = 1.0, phase_shift: float = 0.0,
|
|
153
|
+
S_max: float | None = None):
|
|
154
|
+
super().__init__(name, R, X, S_max)
|
|
155
|
+
self.tap_ratio = tap_ratio
|
|
156
|
+
self.phase_shift = phase_shift
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scipy.optimize as opt
|
|
5
|
+
import networkx as nx
|
|
6
|
+
|
|
7
|
+
from src.fourpace.model import BusComponent, SynchronousMachine, Load, BranchComponent, TransmissionLine, Transformer
|
|
8
|
+
|
|
9
|
+
class Bus(nx.Graph):
|
|
10
|
+
def __init__(self, name: str, Vbase: float, bus_type: str = 'PQ'):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.name = name
|
|
13
|
+
self.Vbase: float | None = Vbase
|
|
14
|
+
|
|
15
|
+
self.type = bus_type
|
|
16
|
+
self.v:float = 1.0
|
|
17
|
+
self.phi:float = 0.0
|
|
18
|
+
self.add_node(name, obj=self)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def P(self) -> float:
|
|
22
|
+
total_p = 0.0
|
|
23
|
+
for _, data in self.nodes(data=True):
|
|
24
|
+
obj = data.get('obj')
|
|
25
|
+
if obj is not self and hasattr(obj, 'P'):
|
|
26
|
+
total_p += obj.P
|
|
27
|
+
return total_p
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def Q(self) -> float:
|
|
31
|
+
total_q = 0.0
|
|
32
|
+
for _, data in self.nodes(data=True):
|
|
33
|
+
obj = data.get('obj')
|
|
34
|
+
if obj is not self and hasattr(obj, 'Q'):
|
|
35
|
+
total_q += obj.Q
|
|
36
|
+
return total_q
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def S(self) -> complex:
|
|
40
|
+
return self.P + 1j*self.Q;
|
|
41
|
+
|
|
42
|
+
def flat_start(self):
|
|
43
|
+
self.V = 1.0
|
|
44
|
+
self.phi = 0.0
|
|
45
|
+
|
|
46
|
+
def get(self):
|
|
47
|
+
return [self.v, self.phi, self.P, self.Q]
|
|
48
|
+
|
|
49
|
+
def add_component(self, component: BusComponent):
|
|
50
|
+
self.add_node(component.name, obj=component)
|
|
51
|
+
self.add_edge(self.name, component.name)
|
|
52
|
+
|
|
53
|
+
def add_components(self, components: list[BusComponent]):
|
|
54
|
+
for component in components:
|
|
55
|
+
self.add_component(component)
|
|
56
|
+
|
|
57
|
+
def total_cost(self) -> float:
|
|
58
|
+
components = [component for _, component in self.nodes(data='obj')]
|
|
59
|
+
components.remove(self)
|
|
60
|
+
sum = 0
|
|
61
|
+
for com in components:
|
|
62
|
+
sum += com.cost()
|
|
63
|
+
return sum
|
|
64
|
+
|
|
65
|
+
class Grid(nx.Graph):
|
|
66
|
+
def __init__(self, Sbase: float):
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.Ybus: np.ndarray | None = None
|
|
69
|
+
self.Sbase: float = Sbase
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def load(cls, filepath: str) -> 'Grid':
|
|
73
|
+
path = Path(filepath)
|
|
74
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
75
|
+
if path.suffix in ['.yaml', '.yml']:
|
|
76
|
+
data = yaml.safe_load(f)
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("❌ Unsupported format! Please use .yaml or .json")
|
|
79
|
+
|
|
80
|
+
grid = cls(Sbase=data.get('Sbase', 100.0))
|
|
81
|
+
|
|
82
|
+
comp_classes = {
|
|
83
|
+
'SynchronousMachine': SynchronousMachine,
|
|
84
|
+
'Load': Load,
|
|
85
|
+
'TransmissionLine': TransmissionLine,
|
|
86
|
+
'Transformer': Transformer
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for b_data in data.get('buses', []):
|
|
90
|
+
bus = Bus(name=b_data['name'], Vbase=b_data['Vbase'], bus_type=b_data.get('bus_type', 'PQ'))
|
|
91
|
+
grid.add_bus(bus)
|
|
92
|
+
|
|
93
|
+
for comp_data in b_data.get('components', []):
|
|
94
|
+
comp_type = comp_data.pop('type')
|
|
95
|
+
if comp_type in comp_classes:
|
|
96
|
+
comp_obj = comp_classes[comp_type](**comp_data)
|
|
97
|
+
grid.bus(bus.name).add_component(comp_obj)
|
|
98
|
+
|
|
99
|
+
for branch_data in data.get('branches', []):
|
|
100
|
+
branch_type = branch_data.pop('type')
|
|
101
|
+
from_bus = branch_data.pop('from_bus')
|
|
102
|
+
to_bus = branch_data.pop('to_bus')
|
|
103
|
+
|
|
104
|
+
if branch_type in comp_classes:
|
|
105
|
+
branch_obj = comp_classes[branch_type](**branch_data)
|
|
106
|
+
grid.connect(from_bus, to_bus, branch_obj)
|
|
107
|
+
|
|
108
|
+
print(f"✅ Successfully loaded grid from {path.name}")
|
|
109
|
+
return grid
|
|
110
|
+
|
|
111
|
+
def add_bus(self, bus:Bus):
|
|
112
|
+
self.add_node(bus.name, bus=bus)
|
|
113
|
+
|
|
114
|
+
def add_busses(self, busses:list[Bus]):
|
|
115
|
+
for bus in busses:
|
|
116
|
+
self.add_bus(bus)
|
|
117
|
+
|
|
118
|
+
def connect(self, from_bus:str, to_bus:str, branch: BranchComponent):
|
|
119
|
+
self.add_edge(from_bus, to_bus, obj=branch)
|
|
120
|
+
|
|
121
|
+
def bus(self, name: str) -> Bus:
|
|
122
|
+
if name in self.nodes:
|
|
123
|
+
return self.nodes[name]['bus']
|
|
124
|
+
|
|
125
|
+
raise KeyError(f"❌ Bus '{name}' not found in this grid.")
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def busses(self) -> list[Bus]:
|
|
129
|
+
return [bus for _, bus in self.nodes(data='bus')]
|
|
130
|
+
|
|
131
|
+
def build_ybus(self) -> np.ndarray:
|
|
132
|
+
nodes = list(self.nodes)
|
|
133
|
+
n = len(nodes)
|
|
134
|
+
Y = np.zeros((n, n), dtype=complex)
|
|
135
|
+
|
|
136
|
+
for u, v, data in self.edges(data=True):
|
|
137
|
+
i = nodes.index(u)
|
|
138
|
+
j = nodes.index(v)
|
|
139
|
+
obj = data.get('obj')
|
|
140
|
+
|
|
141
|
+
if obj is None: continue
|
|
142
|
+
|
|
143
|
+
y = obj.Y
|
|
144
|
+
|
|
145
|
+
if isinstance(obj, TransmissionLine):
|
|
146
|
+
b_sh = obj.B_shunt
|
|
147
|
+
Y[i, i] += y + (1j * b_sh / 2)
|
|
148
|
+
Y[j, j] += y + (1j * b_sh / 2)
|
|
149
|
+
Y[i, j] -= y
|
|
150
|
+
Y[j, i] -= y
|
|
151
|
+
|
|
152
|
+
elif isinstance(obj, Transformer):
|
|
153
|
+
a = obj.tap_ratio
|
|
154
|
+
alpha = obj.phase_shift
|
|
155
|
+
|
|
156
|
+
tap_complex = a * np.exp(1j * alpha)
|
|
157
|
+
|
|
158
|
+
Y[i, i] += y / (a**2)
|
|
159
|
+
Y[j, j] += y
|
|
160
|
+
Y[i, j] -= y / np.conj(tap_complex)
|
|
161
|
+
Y[j, i] -= y / tap_complex
|
|
162
|
+
|
|
163
|
+
self.Ybus = Y
|
|
164
|
+
return Y
|
|
165
|
+
|
|
166
|
+
def result(self):
|
|
167
|
+
print("\n=== 📊 POWER FLOW RESULTS ===")
|
|
168
|
+
for i, bus in enumerate(self.busses):
|
|
169
|
+
P_pu, Q_pu = self.calculate_PQ(i)
|
|
170
|
+
|
|
171
|
+
P_actual = P_pu * self.Sbase
|
|
172
|
+
Q_actual = Q_pu * self.Sbase
|
|
173
|
+
|
|
174
|
+
print(f"Bus {bus.name} | V = {bus.v:.4f} pu | phi = {bus.phi:8.4f} rad | P = {P_actual:7.2f} MW | Q = {Q_actual:7.2f} MVAr")
|
|
175
|
+
|
|
176
|
+
def flat_start(self):
|
|
177
|
+
for bus in self.busses:
|
|
178
|
+
bus.flat_start()
|
|
179
|
+
|
|
180
|
+
def update_busses(self, correction: np.ndarray, damping = .5):
|
|
181
|
+
pv_pq_idx = [i for i, b in enumerate(self.busses) if b.type in ['PV', 'PQ']]
|
|
182
|
+
pq_idx = [i for i, b in enumerate(self.busses) if b.type == 'PQ']
|
|
183
|
+
|
|
184
|
+
n1 = len(pv_pq_idx)
|
|
185
|
+
|
|
186
|
+
for row_i, i in enumerate(pv_pq_idx):
|
|
187
|
+
self.busses[i].phi += damping * float(correction[row_i])
|
|
188
|
+
|
|
189
|
+
for row_i, i in enumerate(pq_idx):
|
|
190
|
+
self.busses[i].v += damping * float(correction[n1 + row_i])
|
|
191
|
+
|
|
192
|
+
def calculate_PQ(self, i):
|
|
193
|
+
bus_i = self.busses[i]
|
|
194
|
+
Vi = bus_i.v
|
|
195
|
+
phi_i = bus_i.phi
|
|
196
|
+
|
|
197
|
+
G = self.Ybus.real
|
|
198
|
+
B = self.Ybus.imag
|
|
199
|
+
|
|
200
|
+
P_i = 0
|
|
201
|
+
Q_i = 0
|
|
202
|
+
|
|
203
|
+
for j in range(len(self.busses)):
|
|
204
|
+
bus_j = self.busses[j]
|
|
205
|
+
Vj = bus_j.v
|
|
206
|
+
phi_j = bus_j.phi
|
|
207
|
+
delta_ij = phi_i - phi_j
|
|
208
|
+
|
|
209
|
+
P_i += Vi * Vj * (G[i, j] * np.cos(delta_ij) + B[i, j] * np.sin(delta_ij))
|
|
210
|
+
Q_i += Vi * Vj * (G[i, j] * np.sin(delta_ij) - B[i, j] * np.cos(delta_ij))
|
|
211
|
+
|
|
212
|
+
return P_i, Q_i
|
|
213
|
+
|
|
214
|
+
def jacobian(self):
|
|
215
|
+
n = len(self.busses)
|
|
216
|
+
G = self.Ybus.real
|
|
217
|
+
B = self.Ybus.imag
|
|
218
|
+
|
|
219
|
+
pv_pq_idx = [i for i, b in enumerate(self.busses) if b.type in ['PV', 'PQ']]
|
|
220
|
+
pq_idx = [i for i, b in enumerate(self.busses) if b.type == 'PQ']
|
|
221
|
+
|
|
222
|
+
n1 = len(pv_pq_idx)
|
|
223
|
+
n2 = len(pq_idx)
|
|
224
|
+
J = np.zeros((n1 + n2, n1 + n2))
|
|
225
|
+
|
|
226
|
+
for row_i, i in enumerate(pv_pq_idx):
|
|
227
|
+
Vi = self.busses[i].v
|
|
228
|
+
phi_i = self.busses[i].phi
|
|
229
|
+
|
|
230
|
+
for col_j, j in enumerate(pv_pq_idx):
|
|
231
|
+
Vj = self.busses[j].v
|
|
232
|
+
phi_j = self.busses[j].phi
|
|
233
|
+
delta_ij = phi_i - phi_j
|
|
234
|
+
|
|
235
|
+
if i == j: # Diagonal elements
|
|
236
|
+
# dPi/dphi_i = -Qi - (Vi^2 * Bii)
|
|
237
|
+
_, Qi_calc = self.calculate_PQ(i)
|
|
238
|
+
J[row_i, col_j] = -Qi_calc - (Vi**2 * B[i, i])
|
|
239
|
+
else: # Off-diagonal
|
|
240
|
+
# dPi/dphi_j = Vi*Vj*(Gij*sin(d_ij) - Bij*cos(d_ij))
|
|
241
|
+
J[row_i, col_j] = Vi * Vj * (G[i, j] * np.sin(delta_ij) - B[i, j] * np.cos(delta_ij))
|
|
242
|
+
|
|
243
|
+
for row_i, i in enumerate(pv_pq_idx):
|
|
244
|
+
Vi = self.busses[i].v
|
|
245
|
+
phi_i = self.busses[i].phi
|
|
246
|
+
|
|
247
|
+
for col_j, j in enumerate(pq_idx):
|
|
248
|
+
Vj = self.busses[j].v
|
|
249
|
+
phi_j = self.busses[j].phi
|
|
250
|
+
delta_ij = phi_i - phi_j
|
|
251
|
+
|
|
252
|
+
if i == j: # Diagonal
|
|
253
|
+
# dPi/dVi = (Pi/Vi) + (Gii * Vi)
|
|
254
|
+
Pi_calc, _ = self.calculate_PQ(i)
|
|
255
|
+
J[row_i, n1 + col_j] = (Pi_calc / Vi) + (G[i, i] * Vi)
|
|
256
|
+
else: # Off-diagonal
|
|
257
|
+
# dPi/dVj = Vi*(Gij*cos(d_ij) + Bij*sin(d_ij))
|
|
258
|
+
J[row_i, n1 + col_j] = Vi * (G[i, j] * np.cos(delta_ij) + B[i, j] * np.sin(delta_ij))
|
|
259
|
+
|
|
260
|
+
for row_i, i in enumerate(pq_idx):
|
|
261
|
+
Vi = self.busses[i].v
|
|
262
|
+
phi_i = self.busses[i].phi
|
|
263
|
+
Pi_calc, Qi_calc = self.calculate_PQ(i)
|
|
264
|
+
actual_row = n1 + row_i
|
|
265
|
+
|
|
266
|
+
# dQ/dphi
|
|
267
|
+
for col_j, j in enumerate(pv_pq_idx):
|
|
268
|
+
Vj = self.busses[j].v
|
|
269
|
+
phi_j = self.busses[j].phi
|
|
270
|
+
delta_ij = phi_i - phi_j
|
|
271
|
+
|
|
272
|
+
if i == j:
|
|
273
|
+
# dQi/dphi_i = Pi - (Vi^2 * Gii)
|
|
274
|
+
Pi_calc, _ = self.calculate_PQ(i)
|
|
275
|
+
J[actual_row, col_j] = Pi_calc - (Vi**2 * G[i, i])
|
|
276
|
+
else:
|
|
277
|
+
# dQi/dphi_j = -Vi*Vj*(Gij*cos(d_ij) + Bij*sin(d_ij))
|
|
278
|
+
J[actual_row, col_j] = -Vi * Vj * (G[i, j] * np.cos(delta_ij) + B[i, j] * np.sin(delta_ij))
|
|
279
|
+
|
|
280
|
+
# dQ/dV
|
|
281
|
+
for col_j, j in enumerate(pq_idx):
|
|
282
|
+
Vj = self.busses[j].v
|
|
283
|
+
phi_j = self.busses[j].phi
|
|
284
|
+
delta_ij = phi_i - phi_j
|
|
285
|
+
|
|
286
|
+
if i == j:
|
|
287
|
+
J[actual_row, n1 + col_j] = (Qi_calc / Vi) - (B[i, i] * Vi)
|
|
288
|
+
else:
|
|
289
|
+
delta_ij = phi_i - phi_j
|
|
290
|
+
J[actual_row, n1 + col_j] = Vi * (G[i, j] * np.sin(delta_ij) - B[i, j] * np.cos(delta_ij))
|
|
291
|
+
|
|
292
|
+
return J
|
|
293
|
+
|
|
294
|
+
def mismatch(self):
|
|
295
|
+
delta_P = []
|
|
296
|
+
delta_Q = []
|
|
297
|
+
|
|
298
|
+
for i, bus in enumerate(self.busses):
|
|
299
|
+
if bus.type == 'Slack':
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
P_calc, Q_calc = self.calculate_PQ(i)
|
|
303
|
+
|
|
304
|
+
delta_P.append((bus.P/self.Sbase) - P_calc)
|
|
305
|
+
|
|
306
|
+
if bus.type == 'PQ':
|
|
307
|
+
delta_Q.append((bus.Q/self.Sbase) - Q_calc)
|
|
308
|
+
return np.array(delta_P + delta_Q).flatten()
|
|
309
|
+
|
|
310
|
+
def solve(self, tolerance=1e-6, max_iteration=100):
|
|
311
|
+
self.flat_start()
|
|
312
|
+
self.build_ybus()
|
|
313
|
+
|
|
314
|
+
for i in range(max_iteration):
|
|
315
|
+
m = self.mismatch()
|
|
316
|
+
if np.max(np.abs(m)) < tolerance:
|
|
317
|
+
print(f"\n✅ Power Flow Converged in {i} iterations!")
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
dx = np.linalg.solve(self.jacobian(), m)
|
|
321
|
+
self.update_busses(dx)
|
|
322
|
+
else:
|
|
323
|
+
print(f"\n⚠️ Warning: Power Flow did not converge within max iterations ({max_iteration}).")
|
|
324
|
+
|
|
325
|
+
self.result()
|
|
326
|
+
|
|
327
|
+
def eco_dispatch(self):
|
|
328
|
+
machines = []
|
|
329
|
+
machine_bus_idx = []
|
|
330
|
+
P_load_bus = np.zeros(len(self.busses))
|
|
331
|
+
Q_load_bus = np.zeros(len(self.busses))
|
|
332
|
+
|
|
333
|
+
for i, bus in enumerate(self.busses):
|
|
334
|
+
for _, data in bus.nodes(data=True):
|
|
335
|
+
obj = data.get('obj')
|
|
336
|
+
if type(obj).__name__ == 'SynchronousMachine':
|
|
337
|
+
machines.append(obj)
|
|
338
|
+
machine_bus_idx.append(i)
|
|
339
|
+
elif type(obj).__name__ == 'Load':
|
|
340
|
+
P_load_bus[i] += abs(obj.P)
|
|
341
|
+
Q_load_bus[i] += abs(obj.Q)
|
|
342
|
+
|
|
343
|
+
num_gen = len(machines)
|
|
344
|
+
num_bus = len(self.busses)
|
|
345
|
+
|
|
346
|
+
if self.Ybus is None:
|
|
347
|
+
self.build_ybus()
|
|
348
|
+
|
|
349
|
+
G = self.Ybus.real
|
|
350
|
+
B = self.Ybus.imag
|
|
351
|
+
|
|
352
|
+
idx_Pg = slice(0, num_gen)
|
|
353
|
+
idx_Qg = slice(num_gen, 2*num_gen)
|
|
354
|
+
idx_V = slice(2*num_gen, 2*num_gen + num_bus)
|
|
355
|
+
idx_theta = slice(2*num_gen + num_bus, 2*num_gen + 2*num_bus)
|
|
356
|
+
|
|
357
|
+
def objective(x):
|
|
358
|
+
Pg = x[idx_Pg]
|
|
359
|
+
cost = 0.0
|
|
360
|
+
for i, m in enumerate(machines):
|
|
361
|
+
if Pg[i] > 0:
|
|
362
|
+
cost += m.a + (m.b * Pg[i]) + (m.c * (Pg[i]**2))
|
|
363
|
+
return cost
|
|
364
|
+
|
|
365
|
+
def power_balance(x):
|
|
366
|
+
Pg = x[idx_Pg]
|
|
367
|
+
Qg = x[idx_Qg]
|
|
368
|
+
V = x[idx_V]
|
|
369
|
+
theta = x[idx_theta]
|
|
370
|
+
|
|
371
|
+
P_gen_bus = np.zeros(num_bus)
|
|
372
|
+
Q_gen_bus = np.zeros(num_bus)
|
|
373
|
+
for i, bus_idx in enumerate(machine_bus_idx):
|
|
374
|
+
P_gen_bus[bus_idx] += Pg[i]
|
|
375
|
+
Q_gen_bus[bus_idx] += Qg[i]
|
|
376
|
+
|
|
377
|
+
P_inj_pu = (P_gen_bus - P_load_bus) / self.Sbase
|
|
378
|
+
Q_inj_pu = (Q_gen_bus - Q_load_bus) / self.Sbase
|
|
379
|
+
|
|
380
|
+
P_calc_pu = np.zeros(num_bus)
|
|
381
|
+
Q_calc_pu = np.zeros(num_bus)
|
|
382
|
+
|
|
383
|
+
for i in range(num_bus):
|
|
384
|
+
for j in range(num_bus):
|
|
385
|
+
delta_ij = theta[i] - theta[j]
|
|
386
|
+
P_calc_pu[i] += V[i] * V[j] * (G[i, j] * np.cos(delta_ij) + B[i, j] * np.sin(delta_ij))
|
|
387
|
+
Q_calc_pu[i] += V[i] * V[j] * (G[i, j] * np.sin(delta_ij) - B[i, j] * np.cos(delta_ij))
|
|
388
|
+
|
|
389
|
+
mismatch_P = P_inj_pu - P_calc_pu
|
|
390
|
+
mismatch_Q = Q_inj_pu - Q_calc_pu
|
|
391
|
+
|
|
392
|
+
return np.concatenate((mismatch_P, mismatch_Q))
|
|
393
|
+
|
|
394
|
+
def line_limits(x):
|
|
395
|
+
V = x[idx_V]
|
|
396
|
+
theta = x[idx_theta]
|
|
397
|
+
V_complex = V * np.exp(1j * theta)
|
|
398
|
+
|
|
399
|
+
margins = []
|
|
400
|
+
nodes_list = list(self.nodes)
|
|
401
|
+
|
|
402
|
+
for u, v, data in self.edges(data=True):
|
|
403
|
+
obj = data.get('obj')
|
|
404
|
+
if obj and getattr(obj, 'S_max', None) is not None:
|
|
405
|
+
i = nodes_list.index(u)
|
|
406
|
+
j = nodes_list.index(v)
|
|
407
|
+
|
|
408
|
+
Vi = V_complex[i]
|
|
409
|
+
Vj = V_complex[j]
|
|
410
|
+
|
|
411
|
+
if type(obj).__name__ == 'Transformer':
|
|
412
|
+
t = obj.tap_ratio * np.exp(1j * obj.phase_shift)
|
|
413
|
+
I_ij = (Vi/t - Vj) * obj.Y
|
|
414
|
+
else:
|
|
415
|
+
I_ij = (Vi - Vj) * obj.Y
|
|
416
|
+
|
|
417
|
+
# Apparent Power (S = V * I*)
|
|
418
|
+
S_flow_pu = abs(Vi * np.conj(I_ij))
|
|
419
|
+
S_flow_MVA = S_flow_pu * self.Sbase
|
|
420
|
+
|
|
421
|
+
margins.append(obj.S_max - S_flow_MVA)
|
|
422
|
+
|
|
423
|
+
if not margins:
|
|
424
|
+
return [1.0]
|
|
425
|
+
return np.array(margins)
|
|
426
|
+
|
|
427
|
+
bounds = []
|
|
428
|
+
|
|
429
|
+
for m in machines:
|
|
430
|
+
p_min = m.Pmin if m.Pmin != float('-inf') else 0.0
|
|
431
|
+
p_max = m.Pmax if m.Pmax != float('inf') else 9999.0
|
|
432
|
+
bounds.append((p_min, p_max))
|
|
433
|
+
|
|
434
|
+
for m in machines:
|
|
435
|
+
q_min = m.Qmin if m.Qmin != float('-inf') else -9999.0
|
|
436
|
+
q_max = m.Qmax if m.Qmax != float('inf') else 9999.0
|
|
437
|
+
bounds.append((q_min, q_max))
|
|
438
|
+
|
|
439
|
+
for bus in self.busses:
|
|
440
|
+
bounds.append((0.95, 1.05))
|
|
441
|
+
|
|
442
|
+
slack_idx = next(i for i, b in enumerate(self.busses) if b.type == 'Slack')
|
|
443
|
+
for i in range(num_bus):
|
|
444
|
+
if i == slack_idx:
|
|
445
|
+
bounds.append((0.0, 0.0))
|
|
446
|
+
else:
|
|
447
|
+
bounds.append((-np.pi, np.pi))
|
|
448
|
+
|
|
449
|
+
Pg0 = [m.Pmax / 2 if m.Pmax != float('inf') else 50.0 for m in machines]
|
|
450
|
+
Qg0 = [0.0 for m in machines]
|
|
451
|
+
V0 = [1.0 for b in self.busses]
|
|
452
|
+
theta0 = [0.0 for b in self.busses]
|
|
453
|
+
x0 = np.concatenate((Pg0, Qg0, V0, theta0))
|
|
454
|
+
|
|
455
|
+
constraints = [
|
|
456
|
+
{'type': 'eq', 'fun': power_balance},
|
|
457
|
+
{'type': 'ineq', 'fun': line_limits}
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
print("\n⏳ Running AC Optimal Power Flow...")
|
|
461
|
+
result = opt.minimize(objective, x0, bounds=bounds, constraints=constraints,
|
|
462
|
+
method='SLSQP', options={'maxiter': 500, 'ftol': 1e-6})
|
|
463
|
+
|
|
464
|
+
if result.success:
|
|
465
|
+
print("✅ ACOPF Converged Successfully!")
|
|
466
|
+
Pg_opt = result.x[idx_Pg]
|
|
467
|
+
Qg_opt = result.x[idx_Qg]
|
|
468
|
+
V_opt = result.x[idx_V]
|
|
469
|
+
theta_opt = result.x[idx_theta]
|
|
470
|
+
|
|
471
|
+
for i, m in enumerate(machines):
|
|
472
|
+
m.P = Pg_opt[i]
|
|
473
|
+
m.Q = Qg_opt[i]
|
|
474
|
+
for i, bus in enumerate(self.busses):
|
|
475
|
+
bus.v = V_opt[i]
|
|
476
|
+
bus.phi = theta_opt[i]
|
|
477
|
+
|
|
478
|
+
print(f"Total Optimal Cost: ${result.fun:.2f}/hr")
|
|
479
|
+
self.result()
|
|
480
|
+
else:
|
|
481
|
+
print("❌ ACOPF Failed to Converge!")
|
|
482
|
+
print(result.message)
|