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 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
+ [![Status](https://img.shields.io/badge/Status-Early%20Access-orange.svg)]()
23
+ [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,40 @@
1
+ # ⚡ Four-Quadrant Power Analysis & Computational Engine (4PACE)
2
+
3
+ [![Status](https://img.shields.io/badge/Status-Early%20Access-orange.svg)]()
4
+ [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![Status](https://img.shields.io/badge/Status-Early%20Access-orange.svg)]()
23
+ [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ numpy
2
+ scipy
3
+ networkx
4
+ pyyaml
@@ -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)