@@ -665,3 +665,151 @@ def test_break_long_link_creates_complete_chain(
665665
666666 for idx in inter_ids :
667667 assert optimizer .nodes .loc [idx , "how_added" ] == "long-distance"
668+
669+
670+ # ---------------------------------------------------------------------------
671+ # Full-pipeline integration test
672+ # ---------------------------------------------------------------------------
673+
674+ @pytest .fixture
675+ def simple_grid_payload () -> dict :
676+ """4-consumer grid, hand-calculable layout.
677+
678+ At lat=1.0, lon=10.0 (UTM zone 32):
679+ - 0.00009 deg ≈ 10 m
680+ - 0.00270 deg ≈ 300 m
681+
682+ Layout (approx, power house at origin):
683+
684+ PH(0,0) C0(10m E) C1(10m N) ...295m gap... C2(300m E) C3(300m E, 10m N)
685+
686+ Near cluster (C0, C1): centroid ≈ 7m NE of PH
687+ Far cluster (C2, C3): centroid ≈ 300m E of PH
688+
689+ distribution_cable.max_length=100m → the ~295m near-to-far pole span forces
690+ intermediate poles (ceil(295/100)-1 = 2 poles inserted).
691+ SHS threshold set astronomically high so all consumers stay grid-connected.
692+ max_n_connections=3, so 2 consumers/pole is within limit.
693+ """
694+ return {
695+ "nodes" : {
696+ "latitude" : [1.0 , 1.000090 , 1.0 , 1.000090 , 1.0 ],
697+ "longitude" : [10.000090 , 10.0 , 10.002700 , 10.002700 , 10.0 ],
698+ "node_type" : ["consumer" , "consumer" , "consumer" , "consumer" , "power-house" ],
699+ "consumer_type" : ["household" , "household" , "household" , "household" , "n.a." ],
700+ "consumer_detail" : ["default" , "default" , "default" , "default" , "n.a." ],
701+ "how_added" : ["manual" , "manual" , "manual" , "manual" , "manual" ],
702+ "is_connected" : [True , True , True , True , True ],
703+ "shs_options" : [0 , 0 , 0 , 0 , 0 ],
704+ "custom_specification" : ["" , "" , "" , "" , "" ],
705+ },
706+ "grid_design" : {
707+ "distribution_cable" : {"max_length" : 100.0 , "epc" : 5.0 },
708+ "connection_cable" : {"max_length" : 30.0 , "epc" : 2.0 },
709+ "pole" : {"max_n_connections" : 3 , "epc" : 100.0 },
710+ "mg" : {"epc" : 50.0 },
711+ "shs" : {"include" : True , "max_grid_cost" : 1_000_000.0 },
712+ },
713+ "yearly_demand" : 1_200.0 ,
714+ }
715+
716+
717+
718+ @pytest .mark .integration
719+ def test_optimize_full_pipeline_simple_grid (simple_grid_payload : dict ) -> None :
720+ """End-to-end smoke test: verifies the full optimize() chain on a minimal,
721+ hand-calculable grid.
722+
723+ What this catches:
724+ - Long link breaking: intermediate poles inserted, all distribution links ≤ max_length
725+ - Clustering: all 4 consumers assigned to a pole (parent set)
726+ - Connectivity: every pole reachable from power house via distribution links
727+ - Pole connection limit: n_connection_links ≤ max_n_connections for every pole
728+ - No consumer left behind: all 4 are grid-connected (SHS threshold is very high)
729+ """
730+ pytest .importorskip ("scipy" )
731+ pytest .importorskip ("utm" )
732+ pytest .importorskip ("k_means_constrained" )
733+ pytest .importorskip ("pyproj" )
734+
735+ dist_max = 100.0
736+ conn_max = 30.0
737+ max_n_conn = 3
738+
739+ grid_opt = GridOptimizer (simple_grid_payload )
740+ result = grid_opt .optimize ()
741+
742+ nodes_out = result ["nodes" ]
743+ links_out = result ["links" ]
744+
745+ # --- All 4 input consumers present, connected, and parented ---
746+ consumer_positions = [
747+ i for i , t in enumerate (nodes_out ["node_type" ]) if t == "consumer"
748+ ]
749+ assert len (consumer_positions ) == 4
750+
751+ for i in consumer_positions :
752+ assert nodes_out ["is_connected" ][i ] is True , (
753+ f"Consumer at index { i } should be grid-connected"
754+ )
755+ assert nodes_out ["parent" ][i ] is not None , (
756+ f"Consumer at index { i } should have a parent pole"
757+ )
758+
759+ # --- No link exceeds its cable max length ---
760+ for label , ltype , length in zip (
761+ links_out ["label" ], links_out ["link_type" ], links_out ["length" ]
762+ ):
763+ if ltype == "distribution" :
764+ assert length <= dist_max , (
765+ f"Distribution link { label } length { length :.1f} m > max { dist_max } m"
766+ )
767+ elif ltype == "connection" :
768+ assert length <= conn_max , (
769+ f"Connection link { label } length { length :.1f} m > max { conn_max } m"
770+ )
771+
772+ # --- At least one intermediate (long-distance) pole was inserted ---
773+ long_distance_poles = grid_opt .nodes [
774+ grid_opt .nodes ["how_added" ] == "long-distance"
775+ ]
776+ assert len (long_distance_poles ) >= 1 , (
777+ "No intermediate pole found; long link breaking did not fire"
778+ )
779+
780+ # --- Pole connection-link count within limit ---
781+ for pole_idx , row in grid_opt .nodes [
782+ grid_opt .nodes ["node_type" ] == "pole"
783+ ].iterrows ():
784+ assert row ["n_connection_links" ] <= max_n_conn , (
785+ f"Pole { pole_idx } has { row ['n_connection_links' ]} connection links "
786+ f"(max { max_n_conn } )"
787+ )
788+
789+ # --- Full network connectivity: all poles reachable from power house ---
790+ dist_links = grid_opt .links [grid_opt .links ["link_type" ] == "distribution" ]
791+ power_house_idx = grid_opt .nodes [
792+ grid_opt .nodes ["node_type" ] == "power-house"
793+ ].index [0 ]
794+ all_poles = grid_opt .nodes [
795+ grid_opt .nodes ["node_type" ].isin (["pole" , "power-house" ])
796+ ].index
797+
798+ reachable : set = {power_house_idx }
799+ queue = [power_house_idx ]
800+ while queue :
801+ current = queue .pop ()
802+ neighbors = set (
803+ dist_links [dist_links ["from_node" ] == current ]["to_node" ].tolist ()
804+ + dist_links [dist_links ["to_node" ] == current ]["from_node" ].tolist ()
805+ )
806+ for neighbor in neighbors - reachable :
807+ reachable .add (neighbor )
808+ queue .append (neighbor )
809+
810+ unreachable = [p for p in all_poles if p not in reachable ]
811+ assert not unreachable , (
812+ f"Poles not reachable from power house: { unreachable } "
813+ )
814+
815+
0 commit comments