Skip to content

Commit 24d158a

Browse files
committed
test(grid_optimizer): add full-pipeline integration test for simple grid
Runs optimize() end-to-end on a 4-consumer hand-calculable grid and verifies: - all consumers grid-connected (SHS threshold is astronomically high) - no distribution or connection link exceeds its configured max length - at least one long-distance intermediate pole was inserted by _break_long_link - pole connection-link count stays within max_n_connections - every pole reachable from power house (no disconnected subgraphs)
1 parent d8a2829 commit 24d158a

1 file changed

Lines changed: 148 additions & 0 deletions

File tree

tests/test_grid_optimizer.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)