Skip to content

Commit 55ee6f2

Browse files
committed
improved logging and validation
1 parent 6a028aa commit 55ee6f2

6 files changed

Lines changed: 354 additions & 7 deletions

File tree

examples/small_test_clos.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ components:
5656
core|core: 800G-ZR+
5757
leaf|leaf: 800G-ZR+
5858
core|leaf: 800G-ZR+
59+
leaf|dc: 800G-ZR+
5960
leaf|spine: 800G-DR4
6061
spine|leaf: 1600G-2xDR4
6162

@@ -74,8 +75,8 @@ visualization:
7475
traffic:
7576
enabled: true
7677
model: gravity
77-
gbps_per_mw: 250.0
78-
mw_per_dc_region: 500.0
78+
gbps_per_mw: 400.0
79+
mw_per_dc_region: 400.0
7980
priority_ratios:
8081
0: 1
8182
matrix_name: baseline_traffic_matrix

tests/test_validation.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,52 @@ def test_validate_scenario_yaml_isolated_nodes_flagged():
138138
yaml_text, integrated_graph_path=None, run_ngraph=False
139139
)
140140
assert any("appears isolated" in s for s in issues)
141+
142+
143+
def test_dc_capacity_vs_demand_validation():
144+
# Build a simple scenario with one DC and a DC->PoP adjacency that has limited capacity
145+
data = _minimal_scenario()
146+
# Add one dc_to_pop adjacency with target_capacity 1000
147+
data["network"]["adjacency"] = [
148+
{
149+
"source": "metro1/dc1",
150+
"target": "metro1/pop1",
151+
"pattern": "one_to_one",
152+
"link_params": {
153+
"capacity": 1000.0,
154+
"cost": 1,
155+
"attrs": {
156+
"link_type": "dc_to_pop",
157+
"source_metro": "Denver",
158+
"target_metro": "Denver",
159+
"target_capacity": 1000.0,
160+
},
161+
},
162+
}
163+
]
164+
# Traffic matrix with a single class that demands 1200 out of the DC and 800 into the DC
165+
data["traffic_matrix_set"] = {
166+
"tm": [
167+
{
168+
"source_path": "^metro1/dc1/.*",
169+
"sink_path": "^metro1/dc1/.*",
170+
"mode": "pairwise",
171+
"priority": 0,
172+
"demand": 1200.0,
173+
},
174+
{
175+
"source_path": "^metro2/dc1/.*",
176+
"sink_path": "^metro1/dc1/.*",
177+
"mode": "pairwise",
178+
"priority": 0,
179+
"demand": 800.0,
180+
},
181+
]
182+
}
183+
issues = validate_scenario_dict(data)
184+
# Egress 1200 > capacity 1000 => violation
185+
assert any(
186+
"egress demand" in s and "exceeds adjacency capacity" in s for s in issues
187+
)
188+
# Ingress 800 <= capacity 1000 => no ingress violation for metro1/dc1
189+
assert not any("ingress demand 800.0" in s for s in issues)

topogen/components_lib.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
# Router Chassis Components
2020
"CoreRouter": {
2121
"component_type": "chassis",
22-
"description": "16 slot, 36x800G ports per slot, 576 ports total",
22+
"description": "16 slot, 32x800G ports per slot, 512 ports total",
2323
"capex": 650_000.0,
24-
"power_watts": 23_000.0, # without optics, typical consumption
25-
"power_watts_max": 30_000.0, # without optics, max consumption
26-
"capacity": 460_800.0, # Gbps
27-
"ports": 576,
24+
"power_watts": 22_000.0, # without optics, typical consumption
25+
"power_watts_max": 29_000.0, # without optics, max consumption
26+
"capacity": 409_600.0, # Gbps
27+
"ports": 512,
2828
"attrs": {"role": "core"},
2929
},
3030
"LeafRouter": {

topogen/scenario/graph_pipeline.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def arc_ceil(
152152
key=f"{adj_id}:{i}-{j}",
153153
link_type="intra_metro",
154154
base_capacity=base_capacity,
155+
target_capacity=base_capacity,
155156
cost=cost,
156157
adjacency_id=adj_id,
157158
distance_km=cost,
@@ -275,6 +276,7 @@ def arc_ceil(
275276
key=f"{adj_id}:{dc}-{p}",
276277
link_type="dc_to_pop",
277278
base_capacity=base_capacity,
279+
target_capacity=base_capacity,
278280
cost=cost_arc,
279281
adjacency_id=adj_id,
280282
distance_km=cost_arc,
@@ -345,6 +347,7 @@ def _add_inter_metro_edges(
345347
key=f"{adj_id}:{p}-{q}",
346348
link_type="inter_metro_corridor",
347349
base_capacity=base_capacity,
350+
target_capacity=base_capacity,
348351
cost=cost,
349352
adjacency_id=adj_id,
350353
distance_km=cost,
@@ -822,6 +825,19 @@ def tm_based_size_capacities(
822825
h_factor = float(getattr(sizing_cfg, "headroom", 1.3))
823826
respect_min = bool(getattr(sizing_cfg, "respect_min_base_capacity", True))
824827

828+
# Snapshot pre-adjustment aggregated capacities per directed metro corridor
829+
prev_corridor_caps: dict[tuple[str, str], float] = {}
830+
for _u, _v, _k, data in G.edges(keys=True, data=True):
831+
if str(data.get("link_type")) != "inter_metro_corridor":
832+
continue
833+
src_name = data.get("source_metro")
834+
tgt_name = data.get("target_metro")
835+
if not isinstance(src_name, str) or not isinstance(tgt_name, str):
836+
continue
837+
prev_cap = float(data.get("base_capacity", data.get("capacity", 0.0)))
838+
key = (src_name, tgt_name)
839+
prev_corridor_caps[key] = prev_corridor_caps.get(key, 0.0) + prev_cap
840+
825841
# Apply sized capacities to inter-metro edges in G
826842
for (u_g, v_g, k_g), L in edge_loads.items():
827843
try:
@@ -840,6 +856,55 @@ def tm_based_size_capacities(
840856
prev = float(data.get("base_capacity", 0.0))
841857
new_base = max(prev, sized) if respect_min else sized
842858
G.edges[u_g, v_g, k_g]["base_capacity"] = new_base
859+
# Keep target_capacity aligned with total intended capacity for the adjacency
860+
G.edges[u_g, v_g, k_g]["target_capacity"] = new_base
861+
862+
# Aggregate post-adjustment capacities per directed metro corridor
863+
post_corridor_caps: dict[tuple[str, str], float] = {}
864+
for _u2, _v2, _k2, data in G.edges(keys=True, data=True):
865+
if str(data.get("link_type")) != "inter_metro_corridor":
866+
continue
867+
src_name = data.get("source_metro")
868+
tgt_name = data.get("target_metro")
869+
if not isinstance(src_name, str) or not isinstance(tgt_name, str):
870+
continue
871+
cap = float(data.get("base_capacity", data.get("capacity", 0.0)))
872+
key = (src_name, tgt_name)
873+
post_corridor_caps[key] = post_corridor_caps.get(key, 0.0) + cap
874+
875+
# Log corridor capacity deltas (per directed pair)
876+
if post_corridor_caps:
877+
try:
878+
total_before = sum(
879+
prev_corridor_caps.get(k, 0.0) for k in post_corridor_caps
880+
)
881+
total_after = sum(post_corridor_caps.values())
882+
logger.info(
883+
"TM sizing: corridor capacity totals (Gbps) before=%s after=%s delta=%s",
884+
f"{total_before:,.1f}",
885+
f"{total_after:,.1f}",
886+
f"{(total_after - total_before):,.1f}",
887+
)
888+
# Only emit lines for pairs that changed
889+
for pair in sorted(post_corridor_caps):
890+
before = float(prev_corridor_caps.get(pair, 0.0))
891+
after = float(post_corridor_caps[pair])
892+
if abs(after - before) <= 0.0:
893+
continue
894+
factor = (after / before) if before > 0.0 else float("inf")
895+
src, dst = pair
896+
logger.info(
897+
"TM sizing: %s -> %s corridor capacity %s -> %s (delta=%s, x%s)",
898+
src,
899+
dst,
900+
f"{before:,.1f}",
901+
f"{after:,.1f}",
902+
f"{(after - before):,.1f}",
903+
"inf" if not (factor < float("inf")) else f"{factor:.2f}",
904+
)
905+
except Exception:
906+
# Logging must not affect sizing; swallow any formatting errors
907+
pass
843908

844909
# Compute PoP egress based on sized corridor capacities
845910
pop_egress: dict[str, float] = {}
@@ -881,6 +946,7 @@ def tm_based_size_capacities(
881946
prev = float(data.get("base_capacity", 0.0))
882947
new_base = max(prev, sized) if respect_min else sized
883948
G.edges[u_g, v_g, k_g]["base_capacity"] = new_base
949+
G.edges[u_g, v_g, k_g]["target_capacity"] = new_base
884950

885951
# Derive PoP<->PoP (intra-metro) base capacities
886952
for u_g, v_g, k_g, data in G.edges(keys=True, data=True):
@@ -895,6 +961,7 @@ def tm_based_size_capacities(
895961
prev = float(data.get("base_capacity", 0.0))
896962
new_base = max(prev, sized) if respect_min else sized
897963
G.edges[u_g, v_g, k_g]["base_capacity"] = new_base
964+
G.edges[u_g, v_g, k_g]["target_capacity"] = new_base
898965

899966
logger.info(
900967
"TM sizing: applied capacities (Q=%s Gb/s, h=%s, alpha=%s, beta=%s)",
@@ -990,6 +1057,12 @@ def to_network_sections(
9901057
"source_metro": data.get("source_metro"),
9911058
"target_metro": data.get("target_metro"),
9921059
}
1060+
# Emit total expected capacity for the adjacency (pre per-link split)
1061+
try:
1062+
tcap = float(data.get("target_capacity", data.get("base_capacity", 0.0)))
1063+
except Exception:
1064+
tcap = float(data.get("base_capacity", 0.0))
1065+
attrs["target_capacity"] = tcap
9931066
# Tag this adjacency with a stable site-edge key and optional adjacency id
9941067
attrs["site_edge"] = f"{u}|{v}|{k}"
9951068
if "adjacency_id" in data:

0 commit comments

Comments
 (0)