Skip to content

Commit fa70ffb

Browse files
committed
fixed syntax err
1 parent 046c0cf commit fa70ffb

2 files changed

Lines changed: 133 additions & 76 deletions

File tree

cim_parser.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import time
33
from typing import List
44

5-
from main import ConnectivityNode, ACLineSegment, EnergyConsumer, GridTopology, FastGridEngine
5+
from main import ConnectivityNode, ACLineSegment, EnergyConsumer, GridTopology, FastGridEngine, Terminal, PerLengthPhaseImpedance
66

77
CIM_NS = {'cim': 'http://iec.ch/TC57/2013/CIM-schema-cim16#',
88
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'}
@@ -11,82 +11,75 @@
1111
def parse_cim_xml(file_path: str) -> GridTopology:
1212
"""
1313
Parses a CIM XML (RDF) file and returns a validated GridTopology object.
14-
Note: Real CIM standard uses Terminals to link ConductingEquipment to ConnectivityNodes.
15-
For this simplified example, we're using direct schema extensions. A full CIMTool profile
16-
would generate the precise mapping required.
14+
Translating basic XML elements into the new strict Terminal linkages and Impedance properties.
1715
"""
1816
print(f"[*] Parsing CIM XML file: {file_path}")
1917
tree = ET.parse(file_path)
2018
root = tree.getroot()
2119

2220
nodes: List[ConnectivityNode] = []
21+
terminals: List[Terminal] = []
2322
segments: List[ACLineSegment] = []
2423
consumers: List[EnergyConsumer] = []
24+
impedance_catalog = {}
2525

2626
# 1. Parse Connectivity Nodes
2727
for node_elem in root.findall('cim:ConnectivityNode', CIM_NS):
2828
node_id = node_elem.attrib.get(f"{{{CIM_NS['rdf']}}}ID")
29-
30-
voltage_elem = node_elem.find(
31-
'cim:ConnectivityNode.voltageLevelKV', CIM_NS)
29+
voltage_elem = node_elem.find('cim:ConnectivityNode.voltageLevelKV', CIM_NS)
3230
v_kv = float(voltage_elem.text) if voltage_elem is not None else 20.0
33-
3431
type_elem = node_elem.find('cim:ConnectivityNode.type', CIM_NS)
3532
n_type = type_elem.text if type_elem is not None else 'PQ'
3633

37-
nodes.append(
38-
ConnectivityNode(
39-
id=node_id,
40-
voltage_level_kv=v_kv,
41-
type=n_type))
34+
nodes.append(ConnectivityNode(id=node_id, voltage_level_kv=v_kv, type=n_type))
4235

4336
# 2. Parse ACLineSegments
4437
for line_elem in root.findall('cim:ACLineSegment', CIM_NS):
4538
line_id = line_elem.attrib.get(f"{{{CIM_NS['rdf']}}}ID")
4639

4740
from_node = line_elem.find('cim:ACLineSegment.fromNode', CIM_NS).attrib.get(
48-
f"{CIM_NS['rdf']} resource").strip('#')
41+
f"{{{CIM_NS['rdf']}}}resource").strip('#')
4942
to_node = line_elem.find('cim:ACLineSegment.toNode', CIM_NS).attrib.get(
50-
f"{CIM_NS['rdf']} resource").strip('#')
43+
f"{{{CIM_NS['rdf']}}}resource").strip('#')
5144

52-
length_km = float(
53-
line_elem.find(
54-
'cim:ACLineSegment.length',
55-
CIM_NS).text)
45+
length_km = float(line_elem.find('cim:ACLineSegment.length', CIM_NS).text)
5646
r = float(line_elem.find('cim:ACLineSegment.r', CIM_NS).text)
5747
x = float(line_elem.find('cim:ACLineSegment.x', CIM_NS).text)
5848

49+
# Map to catalog pattern
50+
imp_id = f"imp_{line_id}"
51+
impedance_catalog[imp_id] = PerLengthPhaseImpedance(id=imp_id, r_ohm_per_km=r, x_ohm_per_km=x)
52+
5953
segments.append(ACLineSegment(
6054
id=line_id,
61-
from_node=from_node,
62-
to_node=to_node,
6355
length_km=length_km,
64-
r_ohm_per_km=r,
65-
x_ohm_per_km=x
56+
impedance_ref=imp_id
6657
))
6758

59+
# Build linkages via Terminals
60+
terminals.append(Terminal(id=f"t_{line_id}_1", connectivity_node=from_node, conducting_equipment=line_id))
61+
terminals.append(Terminal(id=f"t_{line_id}_2", connectivity_node=to_node, conducting_equipment=line_id))
62+
6863
# 3. Parse EnergyConsumers
6964
for load_elem in root.findall('cim:EnergyConsumer', CIM_NS):
7065
load_id = load_elem.attrib.get(f"{{{CIM_NS['rdf']}}}ID")
7166

7267
node_ref = load_elem.find('cim:EnergyConsumer.node', CIM_NS).attrib.get(
73-
f"{CIM_NS['rdf']} resource").strip('#')
68+
f"{{{CIM_NS['rdf']}}}resource").strip('#')
7469
p = float(load_elem.find('cim:EnergyConsumer.p', CIM_NS).text)
7570
q = float(load_elem.find('cim:EnergyConsumer.q', CIM_NS).text)
7671

7772
consumers.append(EnergyConsumer(
7873
id=load_id,
79-
node=node_ref,
8074
p_mw=p,
8175
q_mvar=q
8276
))
8377

84-
print(
85-
f"[+] Loaded {
86-
len(nodes)} Nodes, {
87-
len(segments)} Lines, {
88-
len(consumers)} Consumers.")
89-
return GridTopology(nodes=nodes, segments=segments, consumers=consumers)
78+
# Terminal for consumer
79+
terminals.append(Terminal(id=f"t_{load_id}_1", connectivity_node=node_ref, conducting_equipment=load_id))
80+
81+
print(f"[+] Loaded {len(nodes)} Nodes, {len(terminals)} Terminals, {len(segments)} Lines, {len(consumers)} Consumers.")
82+
return GridTopology(nodes=nodes, terminals=terminals, segments=segments, consumers=consumers, impedance_catalog=impedance_catalog)
9083

9184

9285
if __name__ == "__main__":

main.py

Lines changed: 109 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,77 +35,121 @@ class ConnectivityNode(BaseModel):
3535
type: Literal['PQ', 'PV', 'SLACK'] = 'PQ'
3636

3737

38-
class ACLineSegment(BaseModel):
38+
class Terminal(BaseModel):
39+
id: str
40+
connectivity_node: str
41+
conducting_equipment: str
42+
43+
44+
class PerLengthPhaseImpedance(BaseModel):
3945
id: str
40-
from_node: str
41-
to_node: str
42-
length_km: float = Field(..., gt=0)
4346
r_ohm_per_km: float = Field(..., ge=0)
4447
x_ohm_per_km: float = Field(..., ge=0)
4548

46-
@property
47-
def z_total(self) -> complex:
48-
return complex(self.r_ohm_per_km, self.x_ohm_per_km) * self.length_km
49-
5049

51-
class EnergyConsumer(BaseModel):
50+
class ConductingEquipment(BaseModel):
5251
id: str
53-
node: str
52+
53+
54+
class ACLineSegment(ConductingEquipment):
55+
length_km: float = Field(..., gt=0)
56+
impedance_ref: str
57+
58+
def get_z_total(self, catalog: Dict[str,
59+
PerLengthPhaseImpedance]) -> complex:
60+
imp = catalog[self.impedance_ref]
61+
return complex(imp.r_ohm_per_km, imp.x_ohm_per_km) * self.length_km
62+
63+
64+
class EnergyConsumer(ConductingEquipment):
5465
p_mw: float
5566
q_mvar: float
5667

5768

5869
class GridTopology(BaseModel):
5970
nodes: List[ConnectivityNode]
71+
terminals: List[Terminal]
6072
segments: List[ACLineSegment]
6173
consumers: List[EnergyConsumer] = []
74+
impedance_catalog: Dict[str, PerLengthPhaseImpedance] = {}
6275

6376
@model_validator(mode='after')
6477
def validate_connectivity(self):
6578
node_ids = {n.id for n in self.nodes}
66-
for segment in self.segments:
67-
if segment.from_node not in node_ids or segment.to_node not in node_ids:
68-
raise ValueError(
69-
f"[!] Orphaned segment detected: {
70-
segment.id}")
79+
equip_ids = {e.id for e in self.segments + self.consumers}
80+
81+
for t in self.terminals:
82+
if t.connectivity_node not in node_ids:
83+
raise ValueError(f"[!] Invalid Node Ref on Terminal: {t.id}")
84+
if t.conducting_equipment not in equip_ids:
85+
raise ValueError(f"[!] Invalid Equipment Ref on Terminal: {t.id}")
86+
7187
return self
7288

7389
# ==========================================
7490
# 2. TOPOLOGY GENERATOR (Realistic Params)
7591
# ==========================================
7692

7793

78-
def generate_linear_feeder(n_nodes: int,
79-
voltage_kv: float = 20.0) -> Tuple[List[ConnectivityNode],
80-
List[ACLineSegment]]:
94+
def generate_linear_feeder(
95+
n_nodes: int,
96+
voltage_kv: float = 20.0) -> GridTopology:
8197
"""
8298
Generates a realistic suburban feeder.
8399
Spacing: 0.6 km (Standard node/substation spacing)
84100
Cable: NA2XS2Y 1x150 mm² (R=0.20, X=0.12)
85101
"""
102+
103+
# Standard Impedance Catalog
104+
na2xs2y_150 = PerLengthPhaseImpedance(
105+
id="cable_na2xs2y_1x150", r_ohm_per_km=0.20, x_ohm_per_km=0.12
106+
)
107+
catalog = {na2xs2y_150.id: na2xs2y_150}
108+
86109
nodes = [
87110
ConnectivityNode(
88111
id="source",
89112
voltage_level_kv=voltage_kv,
90113
type='SLACK')]
91114
segments = []
115+
terminals = []
92116

93-
prev_id = "source"
117+
prev_node_id = "source"
94118
for i in range(1, n_nodes + 1):
95-
curr_id = f"node_{i}"
96-
nodes.append(ConnectivityNode(id=curr_id, voltage_level_kv=voltage_kv))
119+
curr_node_id = f"node_{i}"
120+
line_id = f"segment_{i}"
97121

122+
nodes.append(
123+
ConnectivityNode(
124+
id=curr_node_id,
125+
voltage_level_kv=voltage_kv))
126+
127+
# The Equipment
98128
segments.append(ACLineSegment(
99-
id=f"segment_{i}",
100-
from_node=prev_id,
101-
to_node=curr_id,
102-
length_km=0.6, # <--- UPDATED: Realistic 600m spacing
103-
r_ohm_per_km=0.20, # <--- UPDATED: Realistic Aluminum Cable Res
104-
x_ohm_per_km=0.12
129+
id=line_id,
130+
length_km=0.6,
131+
impedance_ref=na2xs2y_150.id
105132
))
106-
prev_id = curr_id
107133

108-
return nodes, segments
134+
# The Linkage via Terminals
135+
terminals.append(
136+
Terminal(
137+
id=f"t_{line_id}_1",
138+
connectivity_node=prev_node_id,
139+
conducting_equipment=line_id))
140+
terminals.append(
141+
Terminal(
142+
id=f"t_{line_id}_2",
143+
connectivity_node=curr_node_id,
144+
conducting_equipment=line_id))
145+
146+
prev_node_id = curr_node_id
147+
148+
return GridTopology(
149+
nodes=nodes,
150+
terminals=terminals,
151+
segments=segments,
152+
impedance_catalog=catalog)
109153

110154
# ==========================================
111155
# 3. HIGH-PERFORMANCE PHYSICS KERNEL
@@ -174,10 +218,21 @@ def _compile_topology(self):
174218

175219
n = len(self.grid.nodes)
176220

177-
# 2. Graph Analysis
221+
# 2. Graph Analysis (Bridging via Terminals)
178222
G = nx.Graph()
223+
224+
# Build equipment-to-nodes map
225+
equip_nodes_map = {}
226+
for t in self.grid.terminals:
227+
if t.conducting_equipment not in equip_nodes_map:
228+
equip_nodes_map[t.conducting_equipment] = []
229+
equip_nodes_map[t.conducting_equipment].append(t.connectivity_node)
230+
179231
for segment in self.grid.segments:
180-
G.add_edge(segment.from_node, segment.to_node, z=segment.z_total)
232+
nodes = equip_nodes_map.get(segment.id, [])
233+
if len(nodes) == 2:
234+
z = segment.get_z_total(self.grid.impedance_catalog)
235+
G.add_edge(nodes[0], nodes[1], z=z)
181236

182237
root_id = self.grid.nodes[0].id
183238
bfs_tree = nx.bfs_tree(G, source=root_id)
@@ -199,8 +254,15 @@ def solve(
199254
q_inj = np.zeros(n)
200255

201256
for consumer in active_consumers:
202-
if consumer.node in self.node_map:
203-
idx = self.node_map[consumer.node]
257+
# Find the terminal for this consumer
258+
consumer_node = None
259+
for t in self.grid.terminals:
260+
if t.conducting_equipment == consumer.id:
261+
consumer_node = t.connectivity_node
262+
break
263+
264+
if consumer_node and consumer_node in self.node_map:
265+
idx = self.node_map[consumer_node]
204266
p_inj[idx] += consumer.p_mw
205267
q_inj[idx] += consumer.q_mvar
206268

@@ -244,23 +306,25 @@ def compute_batch_q(self, v_pu_array: np.ndarray) -> np.ndarray:
244306
# A. SETUP (Realistic Suburban Feeder)
245307
#
246308
N_NODES = 20
247-
nodes, segments = generate_linear_feeder(N_NODES, voltage_kv=20.0)
309+
grid = generate_linear_feeder(N_NODES, voltage_kv=20.0)
248310

249311
# 2.5 MW is a realistic heavy load (e.g., small factory or EV park)
250-
end_node_id = nodes[-1].id
251-
consumers = [
252-
EnergyConsumer(
253-
id="heavy_load",
254-
node=end_node_id,
255-
p_mw=2.5,
256-
q_mvar=0.5)]
312+
end_node_id = grid.nodes[-1].id
313+
heavy_load = EnergyConsumer(id="heavy_load", p_mw=2.5, q_mvar=0.5)
314+
315+
# Link load to node via Terminal
316+
load_terminal = Terminal(
317+
id="t_heavy_load_1",
318+
connectivity_node=end_node_id,
319+
conducting_equipment=heavy_load.id)
257320

258-
grid = GridTopology(nodes=nodes, segments=segments, consumers=consumers)
321+
grid.consumers.append(heavy_load)
322+
grid.terminals.append(load_terminal)
259323

260324
# B. SOLVE
261325
t0 = time.perf_counter()
262326
engine = FastGridEngine(grid)
263-
res = engine.solve(consumers)
327+
res = engine.solve(grid.consumers)
264328
solve_ms = (time.perf_counter() - t0) * 1000
265329

266330
# C. ANALYZE
@@ -276,8 +340,8 @@ def compute_batch_q(self, v_pu_array: np.ndarray) -> np.ndarray:
276340

277341
# D. VISUALIZE
278342
#
279-
dist_km = [i * 0.6 for i in range(len(nodes))] # 0.6km segments
280-
v_profile = [abs(res[n.id]) / 20.0 for n in nodes]
343+
dist_km = [i * 0.6 for i in range(len(grid.nodes))] # 0.6km segments
344+
v_profile = [abs(res[n.id]) / 20.0 for n in grid.nodes]
281345

282346
plt.figure(figsize=(10, 5))
283347
plt.plot(dist_km, v_profile, 'o-', color='navy', label='Voltage Profile')

0 commit comments

Comments
 (0)