@@ -133,21 +133,29 @@ def demand_placement_analysis(
133133) -> FlowIterationResult :
134134 """Analyze traffic demand placement success rates.
135135
136- Returns a structured dictionary per iteration containing per-demand offered
137- and placed volumes (in Gbit/s) and an iteration-level summary. This shape
138- is designed for downstream computation of delivered bandwidth percentiles
139- without having to reconstruct per-iteration joint distributions.
136+ Produces per-demand FlowEntry records and an iteration-level summary suitable
137+ for downstream statistics (e.g., delivered percentiles) without reconstructing
138+ joint distributions.
139+
140+ Additionally exposes placement engine counters to aid performance analysis:
141+ - Per-demand: ``FlowEntry.data.policy_metrics`` (dict) with totals collected by
142+ the active FlowPolicy (e.g., ``spf_calls_total``, ``flows_created_total``,
143+ ``reopt_calls_total``, ``place_iterations_total``, ``placed_total``).
144+ - Per-iteration: ``FlowIterationResult.data.iteration_metrics`` aggregating the
145+ same counters across all demands in the iteration.
140146
141147 Args:
142148 network_view: NetworkView with potential exclusions applied.
143149 demands_config: List of demand configurations (serializable dicts).
144150 placement_rounds: Number of placement optimization rounds.
145151 include_flow_details: When True, include cost_distribution per flow.
146- include_used_edges: When True, include set of used edges per demand in entry data.
152+ include_used_edges: When True, include set of used edges per demand in entry data
153+ as ``FlowEntry.data.edges`` with ``edges_kind='used'``.
147154 **kwargs: Ignored. Accepted for interface compatibility.
148155
149156 Returns:
150- FlowIterationResult describing this iteration.
157+ FlowIterationResult describing this iteration. The ``data`` field contains
158+ ``{"iteration_metrics": { ... }}``.
151159 """
152160 # Reconstruct demands from config to avoid passing complex objects
153161 demands = []
@@ -179,6 +187,15 @@ def demand_placement_analysis(
179187 total_demand = 0.0
180188 total_placed = 0.0
181189
190+ # Aggregate iteration-level engine metrics across all demands
191+ iteration_metrics : dict [str , float ] = {
192+ "spf_calls_total" : 0.0 ,
193+ "flows_created_total" : 0.0 ,
194+ "reopt_calls_total" : 0.0 ,
195+ "place_iterations_total" : 0.0 ,
196+ "placed_total" : 0.0 ,
197+ }
198+
182199 for dmd in tm .demands :
183200 offered = float (getattr (dmd , "volume" , 0.0 ))
184201 placed = float (getattr (dmd , "placed_demand" , 0.0 ))
@@ -212,6 +229,24 @@ def demand_placement_analysis(
212229 extra ["edges" ] = sorted (edge_strings )
213230 extra ["edges_kind" ] = "used"
214231
232+ # Always expose per-demand FlowPolicy metrics when available
233+ fp = getattr (dmd , "flow_policy" , None )
234+ if fp is not None :
235+ try :
236+ # Cumulative totals over the policy's lifetime within this iteration
237+ totals : dict [str , float ] = fp .get_metrics () # type: ignore[assignment]
238+ except Exception :
239+ totals = {}
240+ if totals :
241+ extra ["policy_metrics" ] = {k : float (v ) for k , v in totals .items ()}
242+ # Accumulate iteration-level totals across demands on known keys
243+ for key in iteration_metrics .keys ():
244+ if key in totals :
245+ try :
246+ iteration_metrics [key ] += float (totals [key ])
247+ except Exception :
248+ pass
249+
215250 entry = FlowEntry (
216251 source = str (getattr (dmd , "src_node" , "" )),
217252 destination = str (getattr (dmd , "dst_node" , "" )),
@@ -235,7 +270,11 @@ def demand_placement_analysis(
235270 dropped_flows = dropped_flows ,
236271 num_flows = len (flow_entries ),
237272 )
238- return FlowIterationResult (flows = flow_entries , summary = summary )
273+ return FlowIterationResult (
274+ flows = flow_entries ,
275+ summary = summary ,
276+ data = {"iteration_metrics" : iteration_metrics },
277+ )
239278
240279
241280def sensitivity_analysis (
0 commit comments