@@ -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
5869class 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