@@ -47,16 +47,32 @@ fn init_seed_residuals(
4747 residual
4848}
4949
50+ /// Result of one PPR push pass. `truncated` flags whether the
51+ /// `max_pushes` budget cut the iteration short — when true, the
52+ /// returned estimate is biased toward seeds and the absolute scores
53+ /// are not comparable to a converged run on the same graph. Surfaced
54+ /// to Python via `LatencyBreakdown.ppr_truncated` so calibration /
55+ /// final-eval rows can be filtered or flagged for the paper.
56+ struct PprPushResult {
57+ estimate : Vec < f64 > ,
58+ pushes : usize ,
59+ truncated : bool ,
60+ }
61+
5062fn ppr_push_csr (
5163 csr : & CsrGraph ,
5264 seeds : & FxHashSet < FragmentId > ,
5365 alpha : f64 ,
5466 tol : f64 ,
5567 seed_weights : Option < & FxHashMap < FragmentId , f64 > > ,
56- ) -> Vec < f64 > {
68+ ) -> PprPushResult {
5769 let n = csr. n ;
5870 if n == 0 {
59- return Vec :: new ( ) ;
71+ return PprPushResult {
72+ estimate : Vec :: new ( ) ,
73+ pushes : 0 ,
74+ truncated : false ,
75+ } ;
6076 }
6177
6278 let restart = 1.0 - alpha;
@@ -74,9 +90,11 @@ fn ppr_push_csr(
7490
7591 let max_pushes = ( n * PPR . push_scale_factor ) . min ( PPR . max_pushes_cap ) ;
7692 let mut pushes: usize = 0 ;
93+ let mut truncated = false ;
7794
7895 while let Some ( u) = queue. pop_front ( ) {
7996 if pushes >= max_pushes {
97+ truncated = true ;
8098 break ;
8199 }
82100 let ui = u as usize ;
@@ -114,7 +132,26 @@ fn ppr_push_csr(
114132 pushes += 1 ;
115133 }
116134
117- estimate
135+ PprPushResult {
136+ estimate,
137+ pushes,
138+ truncated,
139+ }
140+ }
141+
142+ /// Public PPR result. `truncated` is logical-OR of forward + backward
143+ /// push truncation flags. When true, downstream renormalization
144+ /// (sum-to-1 across nodes) hides the fact that the iteration was cut
145+ /// short by `max_pushes_cap`; the absolute relevance scores are
146+ /// biased toward seeds and not directly comparable across instances.
147+ /// We surface this to Python so calibration / final-eval rows can be
148+ /// filtered in post-analysis (PolyBench / Multi-SWE-bench instances
149+ /// with >20k fragments are the primary suspects).
150+ pub struct PprResult {
151+ pub scores : FxHashMap < FragmentId , f64 > ,
152+ pub truncated : bool ,
153+ pub forward_pushes : usize ,
154+ pub backward_pushes : usize ,
118155}
119156
120157pub fn personalized_pagerank (
@@ -124,26 +161,28 @@ pub fn personalized_pagerank(
124161 tol : f64 ,
125162 forward_blend : f64 ,
126163 seed_weights : Option < & FxHashMap < FragmentId , f64 > > ,
127- ) -> FxHashMap < FragmentId , f64 > {
128- if graph. node_count ( ) == 0 {
129- return FxHashMap :: default ( ) ;
130- }
131-
132- if seeds. is_empty ( ) {
133- return FxHashMap :: default ( ) ;
164+ ) -> PprResult {
165+ if graph. node_count ( ) == 0 || seeds. is_empty ( ) {
166+ return PprResult {
167+ scores : FxHashMap :: default ( ) ,
168+ truncated : false ,
169+ forward_pushes : 0 ,
170+ backward_pushes : 0 ,
171+ } ;
134172 }
135173
136174 let ( fwd_csr, rev_csr) = graph. to_csr ( ) ;
137175
138- let ( forward_est , backward_est ) = rayon:: join (
176+ let ( forward , backward ) = rayon:: join (
139177 || ppr_push_csr ( fwd_csr, seeds, alpha, tol, seed_weights) ,
140178 || ppr_push_csr ( rev_csr, seeds, alpha, tol, seed_weights) ,
141179 ) ;
142180
143181 let n = fwd_csr. n ;
144182 let mut combined = vec ! [ 0.0f64 ; n] ;
145183 for i in 0 ..n {
146- combined[ i] = forward_blend * forward_est[ i] + ( 1.0 - forward_blend) * backward_est[ i] ;
184+ combined[ i] =
185+ forward_blend * forward. estimate [ i] + ( 1.0 - forward_blend) * backward. estimate [ i] ;
147186 }
148187
149188 let total: f64 = combined. iter ( ) . sum ( ) ;
@@ -154,14 +193,19 @@ pub fn personalized_pagerank(
154193 }
155194
156195 let idx_to_node = & fwd_csr. idx_to_node ;
157- let mut result : FxHashMap < FragmentId , f64 > = FxHashMap :: default ( ) ;
196+ let mut scores : FxHashMap < FragmentId , f64 > = FxHashMap :: default ( ) ;
158197 for i in 0 ..n {
159198 if combined[ i] > 0.0 {
160- result . insert ( idx_to_node[ i] . clone ( ) , combined[ i] ) ;
199+ scores . insert ( idx_to_node[ i] . clone ( ) , combined[ i] ) ;
161200 }
162201 }
163202
164- result
203+ PprResult {
204+ scores,
205+ truncated : forward. truncated || backward. truncated ,
206+ forward_pushes : forward. pushes ,
207+ backward_pushes : backward. pushes ,
208+ }
165209}
166210
167211#[ cfg( test) ]
@@ -177,7 +221,7 @@ mod tests {
177221 fn ppr_empty_graph ( ) {
178222 let mut g = Graph :: new ( ) ;
179223 let seeds = FxHashSet :: default ( ) ;
180- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-4 , 0.4 , None ) ;
224+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-4 , 0.4 , None ) . scores ;
181225 assert ! ( result. is_empty( ) ) ;
182226 }
183227
@@ -189,7 +233,7 @@ mod tests {
189233
190234 let mut seeds = FxHashSet :: default ( ) ;
191235 seeds. insert ( a. clone ( ) ) ;
192- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-4 , 0.4 , None ) ;
236+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-4 , 0.4 , None ) . scores ;
193237 assert ! ( ( result[ & a] - 1.0 ) . abs( ) < 1e-6 ) ;
194238 }
195239
@@ -207,7 +251,7 @@ mod tests {
207251
208252 let mut seeds = FxHashSet :: default ( ) ;
209253 seeds. insert ( a. clone ( ) ) ;
210- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , None ) ;
254+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , None ) . scores ;
211255
212256 assert ! ( result[ & a] > result[ & b] ) ;
213257 assert ! ( result[ & b] > result[ & c] ) ;
@@ -228,7 +272,7 @@ mod tests {
228272
229273 let mut seeds = FxHashSet :: default ( ) ;
230274 seeds. insert ( a. clone ( ) ) ;
231- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , None ) ;
275+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , None ) . scores ;
232276
233277 let total: f64 = result. values ( ) . sum ( ) ;
234278 assert ! ( ( total - 1.0 ) . abs( ) < 1e-6 ) ;
@@ -251,7 +295,7 @@ mod tests {
251295 sw. insert ( a. clone ( ) , 0.9 ) ;
252296 sw. insert ( b. clone ( ) , 0.1 ) ;
253297
254- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , Some ( & sw) ) ;
298+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-6 , 0.4 , Some ( & sw) ) . scores ;
255299 assert ! ( result[ & a] > result[ & b] ) ;
256300 }
257301
@@ -274,8 +318,8 @@ mod tests {
274318 let ( mut g2, _) = build_star_graph ( ) ;
275319 let seeds: FxHashSet < FragmentId > = std:: iter:: once ( center) . collect ( ) ;
276320
277- let r1 = personalized_pagerank ( & mut g1, & seeds, 0.6 , 1e-6 , 0.4 , None ) ;
278- let r2 = personalized_pagerank ( & mut g2, & seeds, 0.6 , 1e-6 , 0.4 , None ) ;
321+ let r1 = personalized_pagerank ( & mut g1, & seeds, 0.6 , 1e-6 , 0.4 , None ) . scores ;
322+ let r2 = personalized_pagerank ( & mut g2, & seeds, 0.6 , 1e-6 , 0.4 , None ) . scores ;
279323
280324 assert_eq ! ( r1. len( ) , r2. len( ) ) ;
281325 for ( id, v1) in & r1 {
@@ -290,8 +334,8 @@ mod tests {
290334 let ( mut g_tight, _) = build_star_graph ( ) ;
291335 let seeds: FxHashSet < FragmentId > = std:: iter:: once ( center) . collect ( ) ;
292336
293- let loose = personalized_pagerank ( & mut g_loose, & seeds, 0.6 , 1e-2 , 0.4 , None ) ;
294- let tight = personalized_pagerank ( & mut g_tight, & seeds, 0.6 , 1e-6 , 0.4 , None ) ;
337+ let loose = personalized_pagerank ( & mut g_loose, & seeds, 0.6 , 1e-2 , 0.4 , None ) . scores ;
338+ let tight = personalized_pagerank ( & mut g_tight, & seeds, 0.6 , 1e-6 , 0.4 , None ) . scores ;
295339
296340 let max_diff = loose
297341 . iter ( )
@@ -307,7 +351,7 @@ mod tests {
307351 fn ppr_symmetric_star_assigns_equal_mass_to_leaves ( ) {
308352 let ( mut g, center) = build_star_graph ( ) ;
309353 let seeds: FxHashSet < FragmentId > = std:: iter:: once ( center. clone ( ) ) . collect ( ) ;
310- let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-8 , 0.5 , None ) ;
354+ let result = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-8 , 0.5 , None ) . scores ;
311355
312356 let leaf_scores: Vec < f64 > = ( 0 ..5 )
313357 . map ( |i| result[ & fid ( & format ! ( "leaf_{i}.rs" ) , 1 , 10 ) ] )
@@ -384,8 +428,9 @@ mod tests {
384428 let tol = 1e-8 ;
385429 let blend = 1.0 ;
386430
387- let r_naive = personalized_pagerank ( & mut naive, & seeds, alpha, tol, blend, None ) ;
388- let r_suppressed = personalized_pagerank ( & mut suppressed, & seeds, alpha, tol, blend, None ) ;
431+ let r_naive = personalized_pagerank ( & mut naive, & seeds, alpha, tol, blend, None ) . scores ;
432+ let r_suppressed =
433+ personalized_pagerank ( & mut suppressed, & seeds, alpha, tol, blend, None ) . scores ;
389434
390435 let hub_naive = r_naive. get ( & hub) . copied ( ) . unwrap_or ( 0.0 ) ;
391436 let hub_suppressed = r_suppressed. get ( & hub) . copied ( ) . unwrap_or ( 0.0 ) ;
@@ -449,7 +494,7 @@ mod tests {
449494 }
450495
451496 let seeds: FxHashSet < FragmentId > = nodes. iter ( ) . take ( 2 ) . cloned ( ) . collect ( ) ;
452- let ppr_scores = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-8 , 0.5 , None ) ;
497+ let ppr_scores = personalized_pagerank ( & mut g, & seeds, 0.6 , 1e-8 , 0.5 , None ) . scores ;
453498 let ego_scores = g. ego_graph ( & seeds, 3 ) ;
454499
455500 let common: Vec < FragmentId > = nodes
@@ -535,7 +580,7 @@ mod tests {
535580 g. add_edge ( leaf. clone ( ) , center. clone ( ) , 1.0 ) ;
536581 }
537582 let seeds: FxHashSet < FragmentId > = std:: iter:: once ( center. clone ( ) ) . collect ( ) ;
538- let result = personalized_pagerank ( & mut g, & seeds, alpha, 1e-10 , 0.5 , None ) ;
583+ let result = personalized_pagerank ( & mut g, & seeds, alpha, 1e-10 , 0.5 , None ) . scores ;
539584
540585 let expected_center = 1.0 / ( 1.0 + alpha) ;
541586 let expected_leaf = alpha / ( n_leaves as f64 * ( 1.0 + alpha) ) ;
0 commit comments