@@ -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
209246class 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)
0 commit comments