Skip to content

Commit 70f650a

Browse files
committed
tm-based sizing
1 parent 313a0de commit 70f650a

5 files changed

Lines changed: 450 additions & 1 deletion

File tree

examples/small_test_baseline.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ build:
6666
capacity_allocation:
6767
enabled: false
6868

69+
tm_sizing:
70+
enabled: true
71+
matrix_name: baseline_traffic_matrix
72+
quantum_gbps: 3200.0
73+
headroom: 1.3
74+
alpha_dc_to_pop: 1.2
75+
beta_intra_pop: 0.8
76+
flow_placement: EQUAL_BALANCED
77+
edge_select: ALL_MIN_COST
78+
respect_min_base_capacity: true
79+
6980
components:
7081
assignments:
7182
core:

examples/small_test_clos.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ build:
6767
capacity_allocation:
6868
enabled: false
6969

70+
tm_sizing:
71+
enabled: true
72+
matrix_name: baseline_traffic_matrix
73+
quantum_gbps: 3200.0
74+
headroom: 1.3
75+
alpha_dc_to_pop: 1.2
76+
beta_intra_pop: 0.8
77+
flow_placement: EQUAL_BALANCED
78+
edge_select: ALL_MIN_COST
79+
respect_min_base_capacity: true
80+
7081
components:
7182
assignments:
7283
core:

topogen/config.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ class BuildConfig:
187187
capacity_allocation: "BuildCapacityAllocationConfig" = field( # type: ignore[name-defined]
188188
default_factory=lambda: BuildCapacityAllocationConfig()
189189
)
190+
tm_sizing: "BuildTmSizingConfig" = field( # type: ignore[name-defined]
191+
default_factory=lambda: BuildTmSizingConfig()
192+
)
190193

191194

192195
@dataclass
@@ -205,6 +208,40 @@ class BuildCapacityAllocationConfig:
205208
enabled: bool = False
206209

207210

211+
@dataclass
212+
class BuildTmSizingConfig:
213+
"""Traffic-matrix-based capacity sizing configuration.
214+
215+
When enabled, capacities are sized from an early traffic matrix and ECMP
216+
routing on the site-level graph before DSL expansion. This stage computes
217+
per-corridor loads from DC-to-DC demands, applies headroom, and quantizes
218+
to discrete capacity increments. Local DC-to-PoP and PoP-to-PoP base
219+
capacities are then derived from metro egress.
220+
221+
Attributes:
222+
enabled: Enable TM-based sizing stage.
223+
matrix_name: Traffic matrix name to use. Defaults to ``traffic.matrix_name``.
224+
quantum_gbps: Capacity quantum Q in Gb/s for rounding up.
225+
headroom: Headroom multiplier h applied to corridor loads before quantizing.
226+
alpha_dc_to_pop: Fraction for DC→PoP base capacity relative to PoP egress.
227+
beta_intra_pop: Fraction for intra-metro PoP↔PoP base capacity relative to
228+
the minimum of the two PoP egress values.
229+
flow_placement: Flow splitting policy. One of {"EQUAL_BALANCED", "PROPORTIONAL"}.
230+
edge_select: Path selection policy. One of {"ALL_MIN_COST", "ALL_MIN_COST_WITH_CAP_REMAINING"}.
231+
respect_min_base_capacity: If True, do not size below configured base capacities.
232+
"""
233+
234+
enabled: bool = False
235+
matrix_name: str | None = None
236+
quantum_gbps: float = 3200.0
237+
headroom: float = 1.3
238+
alpha_dc_to_pop: float = 1.2
239+
beta_intra_pop: float = 0.8
240+
flow_placement: str = "EQUAL_BALANCED"
241+
edge_select: str = "ALL_MIN_COST"
242+
respect_min_base_capacity: bool = True
243+
244+
208245
@dataclass
209246
class ComponentAssignment:
210247
"""Component assignment configuration for a network role.
@@ -580,6 +617,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
580617
build_defaults_dict = build_dict.get("build_defaults", {})
581618
build_overrides_list = build_dict.get("build_overrides", [])
582619
capacity_alloc_dict = build_dict.get("capacity_allocation", {})
620+
tm_sizing_dict = build_dict.get("tm_sizing", {})
583621

584622
if not isinstance(build_defaults_dict, dict):
585623
raise ValueError("'build_defaults' must be a dictionary")
@@ -589,6 +627,10 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
589627
capacity_alloc_dict = {}
590628
if not isinstance(capacity_alloc_dict, dict):
591629
raise ValueError("'capacity_allocation' must be a dictionary if provided")
630+
if tm_sizing_dict is None:
631+
tm_sizing_dict = {}
632+
if not isinstance(tm_sizing_dict, dict):
633+
raise ValueError("'tm_sizing' must be a dictionary if provided")
592634

593635
# Parse link parameter configurations
594636
intra_metro_link_dict = build_defaults_dict.get("intra_metro_link", {})
@@ -655,6 +697,41 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
655697
enabled=bool(capacity_alloc_dict.get("enabled", False))
656698
)
657699

700+
# TM-based sizing configuration
701+
_allowed_tm_keys = {
702+
"enabled",
703+
"matrix_name",
704+
"quantum_gbps",
705+
"headroom",
706+
"alpha_dc_to_pop",
707+
"beta_intra_pop",
708+
"flow_placement",
709+
"edge_select",
710+
"respect_min_base_capacity",
711+
}
712+
_tm_extra = set(tm_sizing_dict.keys()) - _allowed_tm_keys
713+
if _tm_extra:
714+
raise ValueError(
715+
f"Unknown keys in 'build.tm_sizing': {sorted(_tm_extra)}. Allowed keys: {sorted(_allowed_tm_keys)}"
716+
)
717+
tm_sizing = BuildTmSizingConfig(
718+
enabled=bool(tm_sizing_dict.get("enabled", False)),
719+
matrix_name=(
720+
str(tm_sizing_dict.get("matrix_name"))
721+
if tm_sizing_dict.get("matrix_name") is not None
722+
else None
723+
),
724+
quantum_gbps=float(tm_sizing_dict.get("quantum_gbps", 3200.0)),
725+
headroom=float(tm_sizing_dict.get("headroom", 1.3)),
726+
alpha_dc_to_pop=float(tm_sizing_dict.get("alpha_dc_to_pop", 1.2)),
727+
beta_intra_pop=float(tm_sizing_dict.get("beta_intra_pop", 0.8)),
728+
flow_placement=str(tm_sizing_dict.get("flow_placement", "EQUAL_BALANCED")),
729+
edge_select=str(tm_sizing_dict.get("edge_select", "ALL_MIN_COST")),
730+
respect_min_base_capacity=bool(
731+
tm_sizing_dict.get("respect_min_base_capacity", True)
732+
),
733+
)
734+
658735
# Normalize list of overrides into slug->override mapping.
659736
# Schema per entry: { metros: [str, ...], <override_fields> }
660737
allowed_override_keys = {
@@ -703,6 +780,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
703780
build_defaults=build_defaults,
704781
build_overrides=normalized_overrides,
705782
capacity_allocation=capacity_allocation,
783+
tm_sizing=tm_sizing,
706784
)
707785

708786
# Handle optional components configuration (assignments only)

topogen/scenario/assembly.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .graph_pipeline import (
1414
assign_per_link_capacity,
1515
build_site_graph,
16+
tm_based_size_capacities,
1617
to_network_sections,
1718
)
1819
from .libraries import _build_blueprints_section, _build_components_section
@@ -59,7 +60,23 @@ def build_scenario(graph: "nx.Graph", config: "TopologyConfig") -> str:
5960
# New graph-based pipeline builds the authoritative site graph first
6061
logger.info("Building site-level MultiGraph")
6162
G = build_site_graph(metros, metro_settings, graph, config)
62-
logger.info("Assigning capacities from defaults split by adjacency")
63+
# Optional TM-based sizing before per-link capacity split
64+
try:
65+
tm_based_size_capacities(G, metros, metro_settings, config)
66+
except Exception:
67+
# Fail fast with clear error; no silent fallback
68+
raise
69+
# Log clearly which capacity path is in effect
70+
try:
71+
tm_enabled = bool(
72+
getattr(getattr(config.build, "tm_sizing", object()), "enabled", False)
73+
)
74+
except Exception:
75+
tm_enabled = False
76+
if tm_enabled:
77+
logger.info("Assigning per-link capacities after TM-based sizing")
78+
else:
79+
logger.info("Assigning per-link capacities from configured base capacities")
6380
assign_per_link_capacity(G, config)
6481

6582
# Determine used blueprints directly from the site graph (source of truth)

0 commit comments

Comments
 (0)