@@ -183,23 +183,44 @@ def __init__(self, grid_opt_json):
183183
184184 def optimize (self ):
185185 print ("Optimizing distribution grid..." )
186+ n_grid_consumers = self ._setup_coordinates ()
187+ print ("Determining number of poles..." )
188+ self ._place_poles (n_grid_consumers )
189+ self ._connect_nodes ()
190+ print ("Determining distribution links..." )
191+ self ._insert_long_link_poles ()
192+ print ("Determining power house location..." )
193+ self ._run_cost_and_shs_loop ()
194+ return self ._process_results ()
195+
196+ def _setup_coordinates (self ) -> int :
197+ """Convert to projected coords, build road graph, clear stale poles.
198+
199+ Returns n_grid_consumers for use by _place_poles.
200+ """
186201 self .convert_lonlat_xy ()
187202 if self .roads is not None :
188203 self ._build_road_graph ()
189204 # Drop ALL poles from previous runs -> _clear_poles() preserves road-sampled poles
190205 self .nodes = self .nodes [self .nodes ["node_type" ] != "pole" ]
191- n_total_consumers = len (self .nodes )
192206 n_shs_consumers = len (self .nodes [self .nodes ["is_connected" ] == False ]) # noqa: E712
193- n_grid_consumers = n_total_consumers - n_shs_consumers
207+ n_grid_consumers = len ( self . nodes ) - n_shs_consumers
194208 self .nodes = self .nodes .sort_index (key = lambda x : x .astype ("int64" ))
209+ return n_grid_consumers
210+
211+ def _place_poles (self , n_grid_consumers : int ):
212+ """Place k-means or road-sampled poles. Handles power-house consumer placeholder.
213+
214+ Stores _power_house_idx on self for use by _connect_nodes.
215+ """
195216 if self .power_house is not None :
196217 power_house_consumers = self ._connect_power_house_consumer_manually (
197218 self .connection_cable_max_length ,
198219 )
199220 self ._placeholder_consumers_for_power_house ()
200221 else :
201222 power_house_consumers = None
202- print ( "Determining number of poles..." )
223+
203224 if self .roads is not None :
204225 self ._place_poles_with_roads ()
205226 else :
@@ -209,52 +230,44 @@ def optimize(self):
209230
210231 if self .power_house is not None :
211232 cluster_label = self .nodes .loc ["100000" , "cluster_label" ]
212- power_house_idx = self .nodes [
233+ self . _power_house_idx = self .nodes [
213234 (self .nodes ["node_type" ] == "pole" )
214235 & (self .nodes ["cluster_label" ] == cluster_label )
215236 ].index
216237 power_house_consumers ["cluster_label" ] = cluster_label
217238 power_house_consumers ["consumer_type" ] = np .nan
218239 self .nodes = pd .concat ([self .nodes , power_house_consumers ])
219240 self ._placeholder_consumers_for_power_house (remove = True )
220- # Drop old power house index to avoid duplicates
221241 self .nodes = self .nodes .drop (index = self .power_house .index )
242+ else :
243+ self ._power_house_idx = None
222244
245+ def _connect_nodes (self ):
246+ """Build MST distribution backbone and connect consumers to their poles."""
223247 self .create_minimum_spanning_tree ()
224248 self .connect_grid_consumers ()
249+ if self .power_house is not None and self ._power_house_idx is not None :
250+ ph = self ._power_house_idx [0 ]
251+ self .nodes .loc [self .nodes .index == ph , "node_type" ] = "power-house"
252+ self .nodes .loc [self .nodes .index == ph , "how_added" ] = "manual"
225253
226- if self .power_house is not None :
227- self .nodes .loc [self .nodes .index == power_house_idx [0 ], "node_type" ] = "power-house"
228- self .nodes .loc [self .nodes .index == power_house_idx [0 ], "how_added" ] = "manual"
229-
230- # Populate self.links with distribution links so find_index_longest_distribution_link
231- # can detect links that exceed max_length. A second call follows after intermediate
232- # poles have been inserted to rebuild distribution links with long links broken.
254+ def _insert_long_link_poles (self ):
255+ """Find over-length distribution links, insert intermediate poles, and reconnect."""
233256 self .connect_grid_poles ()
234- # Find the connection links_df in the network with lengths greater than the
235- # maximum allowed length for `connection` cables, specified by the user.
236257 long_links = self .find_index_longest_distribution_link ()
237- # Add poles to the identified long `distribution` links_df, so that the
238- # distance between all poles remains below the maximum allowed distance.
239258 self ._add_fixed_poles_on_long_links (long_links = long_links )
240- # Update the (lon,lat) coordinates based on the newly inserted poles
241- # which only have (x,y) coordinates.
242259 self .convert_lonlat_xy (inverse = True )
243- # Connect all poles together using the minimum spanning tree algorithm.
244- print ("Determining distribution links..." )
245260 self .connect_grid_poles (long_links = long_links )
246- # Calculate distances of all poles from the load centroid.
247- # Find the location of the power house.
248261 self .add_number_of_distribution_and_connection_cables ()
262+
263+ def _run_cost_and_shs_loop (self ):
264+ """Iteratively place power house, compute costs, and determine SHS consumers."""
249265 n_iter = 2 if self .power_house is None else 1
250- print ("Determining power house location..." )
251266 for i in range (n_iter ):
252267 if self .power_house is None and i == 0 :
253268 self ._select_location_of_power_house ()
254269 self ._set_direction_of_links ()
255- self .allocate_poles_to_branches ()
256- self .allocate_subbranches_to_branches ()
257- self .label_branch_of_consumers ()
270+ self ._build_branch_hierarchy ()
258271 self .determine_cost_per_pole ()
259272 self ._connection_cost_per_consumer ()
260273 self .determine_costs_per_branch ()
@@ -276,8 +289,6 @@ def optimize(self):
276289 else :
277290 break
278291
279- return self ._process_results ()
280-
281292 def _process_results (self ):
282293 """
283294 Returns a json object with processed nodes and links
@@ -911,7 +922,14 @@ def add_number_of_distribution_and_connection_cables(self):
911922 self .nodes ["n_connection_links" ].fillna (0 ).astype (int )
912923 )
913924
914- def allocate_poles_to_branches (self ):
925+ def _build_branch_hierarchy (self ):
926+ """Assign branch and parent_branch labels to all poles and consumers.
927+
928+ Runs the three allocation steps in the required order:
929+ 1. assign branch labels to poles (allocate_poles_to_branches)
930+ 2. assign parent_branch labels to poles (allocate_subbranches_to_branches)
931+ 3. propagate branch labels to consumers (label_branch_of_consumers)
932+ """
915933 poles = self ._poles ().copy ()
916934 leaf_poles = pd .Series (
917935 poles [poles ["n_distribution_links" ] == 1 ].index
@@ -957,7 +975,27 @@ def allocate_poles_to_branches(self):
957975 "branch" ,
958976 ] = power_house
959977
960- def label_branch_of_consumers (self ):
978+ # --- parent_branch assignment ---
979+ poles = self ._poles ().copy ()
980+ self .nodes ["parent_branch" ] = None
981+
982+ if len (poles ["branch" ].unique ()) > 1 :
983+ leaf_poles_idx = poles [poles ["n_distribution_links" ] == 1 ].index
984+ self ._determine_parent_branches (leaf_poles_idx )
985+ poles_excl_ph = poles [poles ["node_type" ] != "power-house" ]
986+ split_poles_idx = poles_excl_ph [
987+ poles_excl_ph ["n_distribution_links" ] > 2 # noqa: PLR2004
988+ ].index
989+ if len (split_poles_idx ) > 0 :
990+ self ._determine_parent_branches (split_poles_idx )
991+
992+ self .nodes .loc [
993+ (self .nodes ["parent_branch" ].isna ())
994+ & (self .nodes ["node_type" ].isin (["pole" , "power-house" ])),
995+ "parent_branch" ,
996+ ] = power_house
997+
998+ # --- propagate branch to consumers ---
961999 branch_map = self .nodes .loc [
9621000 self .nodes .node_type .isin (["pole" , "power-house" ]),
9631001 "branch" ,
@@ -967,7 +1005,7 @@ def label_branch_of_consumers(self):
9671005 "parent" ,
9681006 ].map (branch_map )
9691007
970- def determine_parent_branches (self , start_poles ):
1008+ def _determine_parent_branches (self , start_poles ):
9711009 poles = self ._poles ().copy ()
9721010 for pole in start_poles :
9731011 branch_start_pole = poles [poles .index == pole ]["branch" ].iloc [0 ]
@@ -980,27 +1018,6 @@ def determine_parent_branches(self, start_poles):
9801018 "parent_branch" ,
9811019 ] = parent_branch
9821020
983- def allocate_subbranches_to_branches (self ):
984- poles = self ._poles ().copy ()
985- self .nodes ["parent_branch" ] = None
986- power_house = poles [poles ["node_type" ] == "power-house" ].index [0 ]
987-
988- if len (poles ["branch" ].unique ()) > 1 :
989- leaf_poles = poles [poles ["n_distribution_links" ] == 1 ].index
990- self .determine_parent_branches (leaf_poles )
991- poles_expect_power_house = poles [poles ["node_type" ] != "power-house" ]
992- split_poles = poles_expect_power_house [
993- poles_expect_power_house ["n_distribution_links" ] > 2 # noqa: PLR2004
994- ].index
995- if len (split_poles ) > 0 :
996- self .determine_parent_branches (split_poles )
997-
998- self .nodes .loc [
999- (self .nodes ["parent_branch" ].isna ())
1000- & (self .nodes ["node_type" ].isin (["pole" , "power-house" ])),
1001- "parent_branch" ,
1002- ] = power_house
1003-
10041021 def determine_cost_per_pole (self ):
10051022 poles = self ._poles ().copy ()
10061023 links = self .get_links ().copy ()
0 commit comments