@@ -813,3 +813,133 @@ def test_optimize_full_pipeline_simple_grid(simple_grid_payload: dict) -> None:
813813 )
814814
815815
816+
817+
818+ @pytest .fixture
819+ def shs_grid_payload () -> dict :
820+ """3-consumer grid designed so that the isolated far consumer becomes SHS.
821+
822+ Layout (approx, power house at origin):
823+
824+ PH(0,0) C0(40m E) C1(40m N) ...460m gap... C2(500m E)
825+
826+ Near consumers (C0, C1): placed 40 m from PH — beyond the 30 m
827+ connection-cable auto-attach threshold in _connect_power_house_consumer_manually
828+ — so they go through k-means and get a proper cluster pole.
829+ The near-cluster pole's marginal cost is ~0.4 (well below 1.0) so it is
830+ never cut.
831+
832+ max_n_connections=3 gives 3 placeholder nodes at PH. The binary search in
833+ _find_opt_number_of_poles converges to 3 clusters (PH, near, far), which
834+ cleanly separates C0/C1 from C2. With only 2 clusters the k_means_constrained
835+ capacity check (size_max x n_clusters >= n_samples) would fail.
836+
837+ Far consumer (C2): single consumer, ~500 m from PH.
838+
839+ Cost estimate for C2 (hand-calculated):
840+ yearly_consumption = 1200 / 3 = 400 Wh/year
841+ distribution chain = 5 poles x (epc_pole + ~96m x epc_dist)
842+ = 5 x (100 + 96x5) = 5 x 580 = 2900 currency/year
843+ connection cost C2 = mg.epc = 50
844+ marginal_cost = (2900 + 50) / 400 = 7.4 currency/Wh
845+ max_levelized_cost = max_grid_cost / 1000 = 1000/1000 = 1.0
846+
847+ 7.4 >> 1.0 -> C2 pole is cut, C2 becomes SHS.
848+ Intermediate long-distance poles then cascade-removed by
849+ _cut_leaf_poles_without_connection (no consumers left on that branch).
850+ """
851+ return {
852+ "nodes" : {
853+ # 40 m E / N of PH so _connect_power_house_consumer_manually
854+ # (threshold = connection_cable.max_length = 30 m) does NOT
855+ # grab C0/C1 before k-means runs.
856+ "latitude" : [1.0 , 1.000360 , 1.0 , 1.0 ],
857+ "longitude" : [10.000359 , 10.0 , 10.004493 , 10.0 ],
858+ "node_type" : ["consumer" , "consumer" , "consumer" , "power-house" ],
859+ "consumer_type" : ["household" , "household" , "household" , "n.a." ],
860+ "consumer_detail" : ["default" , "default" , "default" , "n.a." ],
861+ "how_added" : ["manual" , "manual" , "manual" , "manual" ],
862+ "is_connected" : [True , True , True , True ],
863+ "shs_options" : [0 , 0 , 0 , 0 ],
864+ "custom_specification" : ["" , "" , "" , "" ],
865+ },
866+ "grid_design" : {
867+ "distribution_cable" : {"max_length" : 100.0 , "epc" : 5.0 },
868+ "connection_cable" : {"max_length" : 30.0 , "epc" : 2.0 },
869+ # 3 connections/pole -> binary search finds 3 clusters (PH + near + far)
870+ "pole" : {"max_n_connections" : 3 , "epc" : 100.0 },
871+ "mg" : {"epc" : 50.0 },
872+ "shs" : {"include" : True , "max_grid_cost" : 1_000.0 },
873+ },
874+ "yearly_demand" : 1_200.0 ,
875+ }
876+
877+
878+ @pytest .mark .integration
879+ def test_optimize_full_pipeline_shs_consumer (shs_grid_payload : dict ) -> None :
880+ """End-to-end: optimizer assigns isolated far consumer to SHS.
881+
882+ Consumer "2" (C2, 500m east) is too expensive to connect relative to the
883+ SHS threshold — the optimizer should cut its pole and mark it SHS.
884+ Consumers "0" and "1" (near cluster, ~40m from PH) must stay grid-connected.
885+
886+ Also verifies the intermediate long-distance poles are cascade-removed by
887+ _cut_leaf_poles_without_connection after the far cluster pole is cut —
888+ the remaining grid contains only the near cluster.
889+ """
890+ pytest .importorskip ("scipy" )
891+ pytest .importorskip ("utm" )
892+ pytest .importorskip ("k_means_constrained" )
893+ pytest .importorskip ("pyproj" )
894+
895+ grid_opt = GridOptimizer (shs_grid_payload )
896+ result = grid_opt .optimize ()
897+
898+ nodes_out = result ["nodes" ]
899+
900+ label_to_idx = {lbl : i for i , lbl in enumerate (nodes_out ["label" ])}
901+
902+ # --- Near consumers remain grid-connected with a parent ---
903+ for consumer_label in ("0" , "1" ):
904+ i = label_to_idx [consumer_label ]
905+ assert nodes_out ["is_connected" ][i ] is True , (
906+ f"Near consumer { consumer_label } should be grid-connected"
907+ )
908+ assert nodes_out ["parent" ][i ] is not None , (
909+ f"Near consumer { consumer_label } should have a parent pole"
910+ )
911+
912+ # --- Far isolated consumer assigned to SHS ---
913+ i = label_to_idx ["2" ]
914+ assert nodes_out ["is_connected" ][i ] is False , (
915+ "Consumer '2' (500m isolated) should be SHS (is_connected=False)"
916+ )
917+ assert nodes_out ["parent" ][i ] is None , (
918+ "Consumer '2' (SHS) should have no parent"
919+ )
920+
921+ # --- No orphaned poles: every remaining pole reachable from power house ---
922+ dist_links = grid_opt .links [grid_opt .links ["link_type" ] == "distribution" ]
923+ power_house_idx = grid_opt .nodes [
924+ grid_opt .nodes ["node_type" ] == "power-house"
925+ ].index [0 ]
926+ all_poles = grid_opt .nodes [
927+ grid_opt .nodes ["node_type" ].isin (["pole" , "power-house" ])
928+ ].index
929+
930+ reachable : set = {power_house_idx }
931+ queue = [power_house_idx ]
932+ while queue :
933+ current = queue .pop ()
934+ neighbors = set (
935+ dist_links [dist_links ["from_node" ] == current ]["to_node" ].tolist ()
936+ + dist_links [dist_links ["to_node" ] == current ]["from_node" ].tolist ()
937+ )
938+ for neighbor in neighbors - reachable :
939+ reachable .add (neighbor )
940+ queue .append (neighbor )
941+
942+ unreachable = [p for p in all_poles if p not in reachable ]
943+ assert not unreachable , (
944+ f"Poles not reachable from power house after SHS pruning: { unreachable } "
945+ )
0 commit comments