Skip to main content

gammalooprs/subtraction/
overlap_subspace.rs

1use crate::GammaLoopContext;
2use crate::cff::esurface::EsurfaceCollection;
3use crate::cff::esurface::ExistingEsurfaceId;
4use crate::cff::esurface::ExistingThresholds;
5use crate::cff::esurface::esurface_value_is_strictly_inside;
6use crate::graph::FeynmanGraph;
7use crate::graph::Graph;
8use crate::graph::LmbIndex;
9use crate::graph::LoopMomentumBasis;
10use crate::momentum::sample::ExternalFourMomenta;
11use crate::momentum::sample::LoopIndex;
12use crate::momentum::sample::LoopMomenta;
13use crate::momentum::sample::SubspaceData;
14use crate::momentum::signature::LoopExtSignature;
15use crate::momentum::{Rotation, ThreeMomentum};
16use crate::settings::RuntimeSettings;
17use crate::utils::F;
18use crate::utils::Length;
19use crate::utils::compute_shift_part_subspace;
20use ahash::HashMap;
21use ahash::HashMapExt;
22use ahash::HashSet;
23use bincode_trait_derive::Decode;
24use bincode_trait_derive::Encode;
25use clarabel::algebra::*;
26use clarabel::solver::*;
27use eyre::{Result, eyre};
28use itertools::Itertools;
29use linnet::half_edge::involution::EdgeVec;
30use spenso::algebra::algebraic_traits::IsZero;
31use std::fmt::Display;
32use typed_index_collections::TiVec;
33
34#[derive(Debug, Clone, Encode, Decode)]
35#[trait_decode(trait = GammaLoopContext)]
36pub struct OverlapGroup {
37    pub existing_esurfaces: Vec<ExistingEsurfaceId>,
38    pub complement: Vec<ExistingEsurfaceId>,
39    /// LU overlap centers are stored in the current probe and cut-side LMB frame.
40    /// Solver-derived centers therefore require no further rotation at consumption.
41    pub center: LoopMomenta<F<f64>>,
42}
43
44#[derive(Debug, Clone, Encode, Decode)]
45#[trait_decode(trait = GammaLoopContext)]
46pub struct OverlapStructure {
47    pub overlap_groups: Vec<OverlapGroup>,
48    pub existing_esurfaces: ExistingThresholds,
49}
50
51impl Display for OverlapStructure {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        let existing_esurfaces: Vec<_> = self.existing_esurfaces.iter().map(|id| id.0).collect();
54        writeln!(f, "existing esurfaces: {:?}", existing_esurfaces)?;
55
56        for (i, group) in self.overlap_groups.iter().enumerate() {
57            writeln!(f, "Group {}:", i)?;
58            let existing_esurfaces_in_group: Vec<_> = group
59                .existing_esurfaces
60                .iter()
61                .map(|id| self.existing_esurfaces[*id].0)
62                .collect();
63
64            writeln!(f, "center:\n{}", group.center)?;
65            writeln!(
66                f,
67                "existing esurfaces in group: {:?}",
68                existing_esurfaces_in_group
69            )?;
70        }
71
72        Ok(())
73    }
74}
75
76impl OverlapStructure {
77    pub fn fill_in_complements(&mut self) {
78        for group in self.overlap_groups.iter_mut() {
79            group.complement = self
80                .existing_esurfaces
81                .iter_enumerated()
82                .map(|(existing_esurface_id, _)| existing_esurface_id)
83                .filter(|&existing_esurface_id| {
84                    !group.existing_esurfaces.contains(&existing_esurface_id)
85                })
86                .collect();
87        }
88    }
89
90    pub fn new_empty() -> Self {
91        Self {
92            overlap_groups: vec![],
93            existing_esurfaces: ExistingThresholds::new(),
94        }
95    }
96}
97/// Helper struct to construct the socp problem
98struct PropagatorConstraint<'a> {
99    mass_pointer: Option<usize>, // pointer to value of unique mass
100    signature: &'a LoopExtSignature,
101}
102
103impl PropagatorConstraint<'_> {
104    fn get_dimension(&self) -> usize {
105        let mass_value = if self.mass_pointer.is_some() { 1 } else { 0 };
106
107        3 + mass_value + 1
108    }
109}
110
111fn extract_center(
112    global_loop_num: usize,
113    subspace: &SubspaceData,
114    solution: &[f64],
115) -> LoopMomenta<F<f64>> {
116    let len = solution.len();
117    let num_loop_vars = 3 * subspace.loopcount();
118
119    let mut loop_chunks = solution[len - num_loop_vars..].chunks(3);
120
121    (0..global_loop_num)
122        .map(LoopIndex::from)
123        .map(|loop_index| {
124            if subspace.contains_loop_index(loop_index) {
125                let window = loop_chunks.next().expect("not enough loop momenta");
126                ThreeMomentum::new(
127                    F::from_f64(window[0]),
128                    F::from_f64(window[1]),
129                    F::from_f64(window[2]),
130                )
131            } else {
132                ThreeMomentum::new(F(0.0), F(0.0), F(0.0))
133            }
134        })
135        .collect()
136}
137
138fn construct_solver(
139    overlap_input: &OverlapInput,
140    esurfaces_to_consider: &[ExistingEsurfaceId],
141    existing_esurfaces: &ExistingThresholds,
142    loop_moms: &LoopMomenta<F<f64>>,
143    external_momenta: &ExternalFourMomenta<F<f64>>,
144    verbose: bool,
145) -> DefaultSolver {
146    let num_loops = overlap_input.subspace.loopcount();
147
148    let num_loop_vars = 3 * num_loops;
149
150    // first we study the structure of the problem
151    let mut propagator_constraints: Vec<PropagatorConstraint> = Vec::with_capacity(20);
152
153    let mut inequivalent_masses: Vec<F<f64>> = vec![];
154
155    let mut esurface_constraints: Vec<Vec<usize>> = Vec::with_capacity(esurfaces_to_consider.len());
156
157    for existing_esurface_id in esurfaces_to_consider.iter() {
158        let surface_id = existing_esurfaces[*existing_esurface_id];
159
160        let esurface = &overlap_input.thresholds[surface_id];
161        let lmb = overlap_input.subspace.get_lmb(overlap_input.lmbs);
162        let edge_masses = &overlap_input.edge_masses;
163
164        let mut esurface_constraint_indices: Vec<usize> = Vec::with_capacity(6);
165
166        for edge_id in overlap_input
167            .subspace
168            .contains(&esurface.energies, overlap_input.graph)
169        {
170            if let Some(edge_position) = propagator_constraints
171                .iter()
172                .position(|constraint| *constraint.signature == lmb.edge_signatures[edge_id])
173            {
174                esurface_constraint_indices.push(edge_position);
175            } else {
176                let mass_pointer = if edge_masses[edge_id].is_zero() {
177                    None
178                } else {
179                    Some(
180                        if let Some(mass_position) = inequivalent_masses
181                            .iter()
182                            .position(|&x| x == edge_masses[edge_id])
183                        {
184                            mass_position
185                        } else {
186                            inequivalent_masses.push(edge_masses[edge_id]);
187                            inequivalent_masses.len() - 1
188                        },
189                    )
190                };
191
192                let signature = &lmb.edge_signatures[edge_id];
193
194                let propagator_constraint = PropagatorConstraint {
195                    mass_pointer,
196                    signature,
197                };
198
199                propagator_constraints.push(propagator_constraint);
200                esurface_constraint_indices.push(propagator_constraints.len() - 1);
201            };
202        }
203
204        esurface_constraints.push(esurface_constraint_indices);
205    }
206
207    // now we know the structure, so we can put it in matrix form.
208    // variables are stored as [r , x_p_0, ... x_p_m, k1_x, k1_y, ... k_n_x, k_n_y, k_n_z]
209    let propagator_index_offset = 1;
210    let loop_momentum_offset = propagator_index_offset + propagator_constraints.len();
211
212    let num_primal_variables = loop_momentum_offset + num_loop_vars;
213
214    let cone_dimension_sum = propagator_constraints
215        .iter()
216        .map(|constraint| constraint.get_dimension())
217        .sum::<usize>();
218
219    let num_constaints = 1 + esurface_constraints.len() + cone_dimension_sum;
220
221    // quadratic part of the objective function, we don't need this for now
222    let p_matrix: CscMatrix<f64> =
223        CscMatrix::spalloc((num_primal_variables, num_primal_variables), 0);
224
225    // objective function
226    let mut q_vector = vec![0.0; num_primal_variables];
227    q_vector[0] = 1.0;
228
229    // construct the cones
230    let mut cones: Vec<SupportedConeT<f64>> = Vec::with_capacity(1 + propagator_constraints.len());
231    cones.push(NonnegativeConeT(1));
232    cones.push(NonnegativeConeT(esurface_constraints.len()));
233
234    for prop_constraint in &propagator_constraints {
235        cones.push(SecondOrderConeT(prop_constraint.get_dimension()));
236    }
237
238    // write the constaint equations
239
240    // perhaps it is faster to build the sparse matrix directly, but it is also extremely unreadable
241    let mut a_matrix = vec![vec![0.0; num_primal_variables]; num_constaints];
242    let mut b_vector = vec![0.0; num_constaints];
243
244    a_matrix[0][0] = 1.0;
245    // esurface constraints
246    for (constraint_index, (existing_esurface_id, esurface_constraint)) in esurfaces_to_consider
247        .iter()
248        .zip(esurface_constraints.iter())
249        .enumerate()
250    {
251        for prop_index in esurface_constraint {
252            a_matrix[constraint_index + 1][*prop_index + propagator_index_offset] = 1.0;
253        }
254
255        let esurface_id = existing_esurfaces[*existing_esurface_id];
256        let esurface = &overlap_input.thresholds[esurface_id];
257
258        let shift_part = esurface.compute_shift_part_from_momenta_in_subspace(
259            loop_moms,
260            external_momenta,
261            overlap_input.subspace,
262            overlap_input.lmbs,
263            overlap_input.graph,
264            &overlap_input.edge_masses,
265        );
266        b_vector[constraint_index + 1] = -shift_part.0;
267        a_matrix[constraint_index + 1][0] = -1.0;
268    }
269
270    let spatial_part_of_externals = external_momenta.iter().map(|p| p.spatial).collect();
271
272    // propagator constraints
273    let mut vertical_offset = esurface_constraints.len() + 1;
274    for (cone_index, propagator_constraint) in propagator_constraints.iter().enumerate() {
275        a_matrix[vertical_offset][propagator_index_offset + cone_index] = -1.0;
276        vertical_offset += 1;
277
278        let spatial_shift = compute_shift_part_subspace(
279            &propagator_constraint.signature.internal,
280            &propagator_constraint.signature.external,
281            loop_moms,
282            &spatial_part_of_externals,
283            overlap_input.subspace,
284        );
285
286        b_vector[vertical_offset] = spatial_shift.px.0;
287        b_vector[vertical_offset + 1] = spatial_shift.py.0;
288        b_vector[vertical_offset + 2] = spatial_shift.pz.0;
289
290        for (subspace_loop_index, individual_loop_signature) in overlap_input
291            .subspace
292            .project_loop_signature_filtered(&propagator_constraint.signature.internal)
293            .enumerate()
294        {
295            if individual_loop_signature.is_sign() {
296                a_matrix[vertical_offset][loop_momentum_offset + 3 * subspace_loop_index] =
297                    -(individual_loop_signature as i8) as f64;
298                a_matrix[vertical_offset + 1][loop_momentum_offset + 3 * subspace_loop_index + 1] =
299                    -(individual_loop_signature as i8) as f64;
300                a_matrix[vertical_offset + 2][loop_momentum_offset + 3 * subspace_loop_index + 2] =
301                    -(individual_loop_signature as i8) as f64;
302            }
303        }
304
305        vertical_offset += 3;
306
307        if let Some(mass_index) = propagator_constraint.mass_pointer {
308            b_vector[vertical_offset] = inequivalent_masses[mass_index].0;
309            vertical_offset += 1;
310        }
311    }
312
313    let a_matrix_sparse = CscMatrix::from(&a_matrix);
314
315    let settings = DefaultSettingsBuilder::default()
316        .verbose(verbose)
317        .build()
318        .unwrap();
319
320    DefaultSolver::new(
321        &p_matrix,
322        &q_vector,
323        &a_matrix_sparse,
324        &b_vector,
325        &cones,
326        settings,
327    )
328    .unwrap()
329}
330
331pub(crate) fn find_center(
332    overlap_input: &OverlapInput,
333    esurfaces_to_consider: &[ExistingEsurfaceId],
334    existing_esurfaces: &ExistingThresholds,
335    loop_moms: &LoopMomenta<F<f64>>,
336    external_momenta: &ExternalFourMomenta<F<f64>>,
337    verbose: bool,
338) -> Option<LoopMomenta<F<f64>>> {
339    let mut solver = construct_solver(
340        overlap_input,
341        esurfaces_to_consider,
342        existing_esurfaces,
343        loop_moms,
344        external_momenta,
345        verbose,
346    );
347
348    solver.solve();
349
350    let global_loop_number = overlap_input.graph.get_loop_number();
351    let esurfaces_to_check = esurfaces_to_consider
352        .iter()
353        .map(|existing_esurface_id| existing_esurfaces[*existing_esurface_id])
354        .collect();
355
356    if solver.solution.status == SolverStatus::Solved {
357        let center = extract_center(
358            global_loop_number,
359            overlap_input.subspace,
360            &solver.solution.x,
361        );
362        check_global_center(
363            overlap_input,
364            &esurfaces_to_check,
365            &center,
366            loop_moms,
367            external_momenta,
368        )
369        .then_some(center)
370    } else if solver.solution.status == SolverStatus::AlmostSolved
371        || solver.solution.status == SolverStatus::InsufficientProgress
372    {
373        // if the solver did not converge, we check if the solution is still valid
374        let center = extract_center(
375            global_loop_number,
376            overlap_input.subspace,
377            &solver.solution.x,
378        );
379
380        let is_valid = check_global_center(
381            overlap_input,
382            &esurfaces_to_check,
383            &center,
384            loop_moms,
385            external_momenta,
386        );
387
388        if is_valid { Some(center) } else { None }
389    } else {
390        if verbose {
391            println!("{:?}", solver.solution.x);
392        }
393
394        None
395    }
396}
397
398pub(crate) struct OverlapInput<'a> {
399    pub graph: &'a Graph,
400    pub settings: &'a RuntimeSettings,
401    pub subspace: &'a SubspaceData,
402    pub lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
403    pub thresholds: &'a EsurfaceCollection,
404    pub edge_masses: EdgeVec<F<f64>>,
405}
406
407pub(crate) fn check_global_center(
408    overlap_input: &OverlapInput,
409    existing_esurfaces: &ExistingThresholds,
410    center: &LoopMomenta<F<f64>>,
411    loop_moms: &LoopMomenta<F<f64>>,
412    external_momenta: &ExternalFourMomenta<F<f64>>,
413) -> bool {
414    let mut center_with_fixed_complement = loop_moms.clone();
415    for loop_index in overlap_input.subspace.iter_lmb_indices() {
416        center_with_fixed_complement[loop_index] = center[loop_index];
417    }
418
419    existing_esurfaces.iter().all(|esurface_id| {
420        let esurface = &overlap_input.thresholds[*esurface_id];
421
422        let lmb = overlap_input.subspace.get_lmb(overlap_input.lmbs);
423        let edge_masses = &overlap_input.edge_masses;
424
425        let esurface_val = esurface.compute_from_momenta(
426            lmb,
427            edge_masses,
428            &center_with_fixed_complement,
429            external_momenta,
430        );
431
432        esurface_value_is_strictly_inside(&esurface_val, &F(overlap_input.settings.kinematics.e_cm))
433    })
434}
435
436/// Runtime overlap failures are returned so the stability machinery can retry at higher precision.
437/// Structural generation invariants are still asserted where malformed generated data is unrecoverable.
438/// Solver-derived centers are already found in the current probe frame. `probe_rotation` is
439/// needed only for a configured forced center, whose coordinates are defined in the identity
440/// frame and must therefore be rotated exactly once before the cut-side LMB transform.
441pub(crate) fn find_maximal_overlap(
442    overlap_input: &OverlapInput,
443    existing_esurfaces: &ExistingThresholds,
444    loop_moms: &LoopMomenta<F<f64>>,
445    external_momenta: &ExternalFourMomenta<F<f64>>,
446    probe_rotation: &Rotation,
447) -> Result<OverlapStructure> {
448    let mut res = OverlapStructure {
449        overlap_groups: vec![],
450        existing_esurfaces: existing_esurfaces.clone(),
451    };
452
453    let settings = overlap_input.settings;
454
455    let all_existing_esurfaces = existing_esurfaces
456        .iter_enumerated()
457        .map(|a| a.0)
458        .collect_vec();
459
460    if let Some(global_center) = &settings.subtraction.overlap_settings.force_global_center {
461        let global_center_identity: LoopMomenta<F<f64>> = global_center
462            .iter()
463            .map(|coordinates| ThreeMomentum {
464                px: F(coordinates[0]),
465                py: F(coordinates[1]),
466                pz: F(coordinates[2]),
467            })
468            .collect();
469        let rotated_external_spatial = external_momenta
470            .iter()
471            .map(|momentum| momentum.spatial)
472            .collect();
473        // Forced centers are configured in the graph's identity-frame LMB. Apply the probe
474        // rotation once, then express that rotated point in the cut-side LMB used by the solver.
475        let mut global_center_probe = global_center_identity.rotate(probe_rotation).lmb_transform(
476            &overlap_input.graph.loop_momentum_basis,
477            overlap_input.subspace.get_lmb(overlap_input.lmbs),
478            &rotated_external_spatial,
479        );
480        // A subspace center specifies only its active coordinates. The complementary loop
481        // coordinates remain fixed at the sampled point throughout center validation, radial
482        // solving, and final CT reconstruction. Affine LMB transforms can populate inactive
483        // components even when the identity-frame center is zero, so project them out explicitly.
484        for loop_index in (0..global_center_probe.len()).map(LoopIndex::from) {
485            if !overlap_input.subspace.contains_loop_index(loop_index) {
486                global_center_probe[loop_index] = ThreeMomentum::new(F(0.0), F(0.0), F(0.0));
487            }
488        }
489
490        tracing::debug!(
491            graph = %overlap_input.graph.name,
492            rotation_id = %probe_rotation.method,
493            center_provenance = "forced_identity_frame_rotated_lmb_transformed_and_projected_once",
494            identity_center = %global_center_identity,
495            probe_cut_lmb_center = %global_center_probe,
496            "prepared forced LU overlap center"
497        );
498
499        if !settings.subtraction.overlap_settings.check_global_center {
500            tracing::warn!(
501                graph = %overlap_input.graph.name,
502                "overlap_settings.check_global_center=false is deprecated; forced centers are always validated"
503            );
504        }
505
506        let is_valid = check_global_center(
507            overlap_input,
508            existing_esurfaces,
509            &global_center_probe,
510            loop_moms,
511            external_momenta,
512        );
513
514        if !is_valid {
515            return Err(eyre!(
516                "Forced identity-frame center is not finite and strictly inside all existing esurfaces after applying probe rotation {} and the cut-side LMB transform",
517                probe_rotation.method,
518            ));
519        }
520
521        let single_group = OverlapGroup {
522            existing_esurfaces: all_existing_esurfaces,
523            center: global_center_probe,
524            complement: vec![],
525        };
526        res.overlap_groups.push(single_group);
527
528        res.fill_in_complements();
529        return Ok(res);
530    }
531
532    if settings.subtraction.overlap_settings.try_origin {
533        let global_loop_count = overlap_input.graph.get_loop_number();
534        let origin = LoopMomenta::from_iter(
535            (0..global_loop_count).map(|_| ThreeMomentum::new(F(0.0), F(0.0), F(0.0))),
536        );
537
538        let is_valid = check_global_center(
539            overlap_input,
540            existing_esurfaces,
541            &origin,
542            loop_moms,
543            external_momenta,
544        );
545
546        if is_valid {
547            let single_group = OverlapGroup {
548                existing_esurfaces: all_existing_esurfaces,
549                center: origin,
550                complement: vec![],
551            };
552            res.overlap_groups.push(single_group);
553            res.fill_in_complements();
554            return Ok(res);
555        }
556    }
557
558    if settings.subtraction.overlap_settings.try_origin_all_lmbs {
559        todo!("Not all heuristics implemented")
560    }
561
562    // first try if all esurfaces have a single center, we explitely seach a center instead of trying the
563    // origin. This is because the origin might not be optimal.
564    let option_center = find_center(
565        overlap_input,
566        &all_existing_esurfaces,
567        existing_esurfaces,
568        loop_moms,
569        external_momenta,
570        false,
571    );
572
573    if let Some(center) = option_center {
574        let single_group = OverlapGroup {
575            existing_esurfaces: all_existing_esurfaces,
576            center,
577            complement: vec![],
578        };
579        res.overlap_groups.push(single_group);
580        res.fill_in_complements();
581        return Ok(res);
582    }
583
584    // if the center is not valid, create a table of all pairs
585    let esurface_pairs = EsurfacePairs::new(
586        overlap_input,
587        existing_esurfaces,
588        loop_moms,
589        external_momenta,
590    );
591
592    // if settings.general.debug > 3 {
593    //     DEBUG_LOGGER.write("overlap_pairs", &esurface_pairs);
594    // }
595
596    let mut num_disconnected_surfaces = 0;
597
598    for (existing_esurface_id, &esurface_id) in existing_esurfaces.iter_enumerated() {
599        // if an esurface overlaps with no other esurface, it is part of the maximal overlap structure
600        if esurface_pairs.has_no_overlap(existing_esurface_id) {
601            let center = find_center(
602                overlap_input,
603                &[existing_esurface_id],
604                existing_esurfaces,
605                loop_moms,
606                external_momenta,
607                false,
608            )
609            .ok_or_else(|| {
610                let esurface = &overlap_input.thresholds[esurface_id];
611
612                let mut error_message = String::new();
613
614                error_message.push_str(&format!(
615                    "Could not find center of esuface {:?}\n",
616                    esurface_id
617                ));
618                error_message.push_str(&format!("edges: {:?}\n", esurface.energies));
619
620                error_message.push_str(&format!("external shift: {:?}\n", esurface.external_shift));
621
622                error_message.push_str(&format!("External momenta: {:#?}\n", external_momenta));
623
624                eyre!("{}", error_message)
625            })?;
626
627            res.overlap_groups.push(OverlapGroup {
628                existing_esurfaces: vec![existing_esurface_id],
629                center,
630                complement: vec![],
631            });
632            num_disconnected_surfaces += 1;
633        }
634    }
635
636    // if settings.general.debug > 3 {
637    //     DEBUG_LOGGER.write("num_disconnected_surfaces", &num_disconnected_surfaces);
638    // }
639
640    if num_disconnected_surfaces == existing_esurfaces.len() {
641        res.fill_in_complements();
642        return Ok(res);
643    }
644
645    let mut subset_size =
646        if let Some(size) = esurface_pairs.has_pair_with.iter().map(Vec::len).max() {
647            size + 1
648        } else {
649            1
650        };
651
652    while subset_size > 1 {
653        let possible_subsets =
654            esurface_pairs.construct_possible_subsets_of_len(existing_esurfaces, subset_size, &res);
655
656        for subset in possible_subsets.iter() {
657            let option_center = find_center(
658                overlap_input,
659                subset,
660                existing_esurfaces,
661                loop_moms,
662                external_momenta,
663                false,
664            );
665
666            if let Some(center) = option_center {
667                res.overlap_groups.push(OverlapGroup {
668                    existing_esurfaces: subset.clone(),
669                    center,
670                    complement: vec![],
671                });
672            }
673        }
674
675        // if settings.general.debug > 3 {
676        //     DEBUG_LOGGER.write(
677        //         "subset_size_and_num_possible_subsets_and_res",
678        //         &(subset_size, possible_subsets.len(), &res),
679        //     );
680        // }
681
682        subset_size -= 1;
683    }
684
685    res.fill_in_complements();
686    Ok(res)
687}
688
689fn is_subset_of_result(subset: &[ExistingEsurfaceId], result: &OverlapStructure) -> bool {
690    result.overlap_groups.iter().any(|group| {
691        subset
692            .iter()
693            .all(|&x| group.existing_esurfaces.contains(&x))
694    })
695}
696
697#[derive(Debug)]
698struct EsurfacePairs {
699    data: HashMap<(ExistingEsurfaceId, ExistingEsurfaceId), LoopMomenta<F<f64>>>,
700    has_pair_with: Vec<Vec<ExistingEsurfaceId>>,
701}
702
703impl EsurfacePairs {
704    fn insert(
705        &mut self,
706        pair: (ExistingEsurfaceId, ExistingEsurfaceId),
707        center: LoopMomenta<F<f64>>,
708    ) {
709        if pair.0 > pair.1 {
710            self.data.insert((pair.1, pair.0), center);
711        } else {
712            self.data.insert(pair, center);
713        }
714    }
715
716    fn pair_exists(&self, pair: (ExistingEsurfaceId, ExistingEsurfaceId)) -> bool {
717        if pair.0 > pair.1 {
718            self.data.contains_key(&(pair.1, pair.0))
719        } else {
720            self.data.contains_key(&pair)
721        }
722    }
723
724    fn new_empty(num_existing_esurfaces: usize) -> Self {
725        let capacity = match num_existing_esurfaces {
726            0 => 0,
727            1 => 0,
728            _ => num_existing_esurfaces * (num_existing_esurfaces - 1) / 2,
729        };
730
731        Self {
732            data: HashMap::with_capacity(capacity),
733            has_pair_with: vec![Vec::with_capacity(num_existing_esurfaces); num_existing_esurfaces],
734        }
735    }
736
737    fn new(
738        overlap_input: &OverlapInput,
739        existing_esurfaces: &ExistingThresholds,
740        loop_moms: &LoopMomenta<F<f64>>,
741        external_momenta: &ExternalFourMomenta<F<f64>>,
742    ) -> Self {
743        let mut res = Self::new_empty(existing_esurfaces.len());
744
745        let all_existing_esurfaces = existing_esurfaces
746            .iter_enumerated()
747            .map(|a| a.0)
748            .collect_vec();
749
750        for (i, &esurface_id_1) in all_existing_esurfaces.iter().enumerate() {
751            for &esurface_id_2 in all_existing_esurfaces.iter().skip(i + 1) {
752                let center = find_center(
753                    overlap_input,
754                    &[esurface_id_1, esurface_id_2],
755                    existing_esurfaces,
756                    loop_moms,
757                    external_momenta,
758                    false,
759                );
760
761                if let Some(center) = center {
762                    res.insert((esurface_id_1, esurface_id_2), center);
763                    res.has_pair_with[Into::<usize>::into(esurface_id_1)].push(esurface_id_2);
764                    res.has_pair_with[Into::<usize>::into(esurface_id_2)].push(esurface_id_1);
765                }
766            }
767        }
768
769        res
770    }
771
772    fn has_no_overlap(&self, esurface_id: ExistingEsurfaceId) -> bool {
773        self.has_pair_with[Into::<usize>::into(esurface_id)].is_empty()
774    }
775
776    fn construct_possible_subsets_of_len(
777        &self,
778        existing_esurfaces: &ExistingThresholds,
779        subset_len: usize,
780        result: &OverlapStructure,
781    ) -> HashSet<Vec<ExistingEsurfaceId>> {
782        let mut res = HashSet::default();
783        let existing_esurfaces_not_in_overlap = existing_esurfaces
784            .iter_enumerated()
785            .map(|a| a.0)
786            .filter(|&existing_esurface_id| {
787                !result
788                    .overlap_groups
789                    .iter()
790                    .any(|group| group.existing_esurfaces.contains(&existing_esurface_id))
791            });
792
793        let mut possible_options_from_esurfaces_not_in_overlap = HashSet::default();
794
795        for esurface in existing_esurfaces_not_in_overlap.filter(|existing_esurface_id| {
796            self.has_pair_with[Into::<usize>::into(*existing_esurface_id)].len() >= subset_len - 1
797        }) {
798            for possible_combination in self.has_pair_with[Into::<usize>::into(esurface)]
799                .iter()
800                .combinations(subset_len - 1)
801            {
802                let mut option = vec![esurface];
803                option.extend(possible_combination.iter().copied());
804                let mut is_valid = true;
805
806                'pair_loop: for i in 0..subset_len - 1 {
807                    for j in i + 1..subset_len - 1 {
808                        let pair = (
809                            Into::<ExistingEsurfaceId>::into(*possible_combination[i]),
810                            Into::<ExistingEsurfaceId>::into(*possible_combination[j]),
811                        );
812
813                        if !self.pair_exists(pair) {
814                            is_valid = false;
815                            break 'pair_loop;
816                        }
817                    }
818                }
819
820                if is_valid {
821                    option.sort_unstable();
822                    possible_options_from_esurfaces_not_in_overlap.insert(option);
823                }
824            }
825        }
826
827        let existing_pairs_not_in_overlap = self.data.keys().filter(|(left, right)| {
828            !is_subset_of_result(&[*left, *right], result)
829                && is_subset_of_result(&[*left], result)
830                && is_subset_of_result(&[*right], result)
831                && self.has_pair_with[Into::<usize>::into(*left)].len() >= subset_len - 1
832                && self.has_pair_with[Into::<usize>::into(*right)].len() >= subset_len - 1
833        });
834
835        let mut possible_options_from_pairs_not_in_overlap = HashSet::default();
836
837        for pair in existing_pairs_not_in_overlap {
838            let possible_additions = existing_esurfaces
839                .iter_enumerated()
840                .map(|a| a.0)
841                .filter(|&id| {
842                    id != pair.0
843                        && id != pair.1
844                        && self.has_pair_with[Into::<usize>::into(pair.0)].contains(&id)
845                        && self.has_pair_with[Into::<usize>::into(pair.1)].contains(&id)
846                })
847                .combinations(subset_len - 2);
848
849            for possible_addition in possible_additions {
850                let mut option = vec![pair.0, pair.1];
851                option.extend(possible_addition.iter().copied());
852                option.sort_unstable();
853                possible_options_from_pairs_not_in_overlap.insert(option);
854            }
855        }
856
857        res.extend(possible_options_from_esurfaces_not_in_overlap);
858        res.extend(possible_options_from_pairs_not_in_overlap);
859
860        res
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use crate::{
868        cff::{
869            cff_graph::VertexSet,
870            esurface::{Esurface, EsurfaceExistence, EsurfaceID},
871        },
872        dot,
873        graph::{LMBext, parse::from_dot::IntoGraph},
874        initialisation::test_initialise,
875        momentum::{FourMomentum, Rotatable, Rotation, RotationMethod},
876    };
877    use linnet::half_edge::involution::EdgeIndex;
878    use linnet::half_edge::subgraph::{SuBitGraph, SubSetOps};
879    use typed_index_collections::ti_vec;
880
881    #[test]
882    fn global_center_check_preserves_fixed_complement_for_multidimensional_subspace() {
883        test_initialise().unwrap();
884        let graph: Graph = dot!(digraph subspace_center {
885            ext [style=invis]
886            edge [num=1 mass=0]
887            node [num=1]
888            ext->v1:0 [id=0]
889            v1->v2 [id=1]
890            v2->v1 [id=2]
891            v1->v2 [id=3]
892            v2->v1 [id=4]
893            ext->v2:1 [id=5]
894        })
895        .unwrap();
896
897        assert_eq!(graph.loop_momentum_basis.loop_edges.len(), 3);
898        let all_lmbs = ti_vec![graph.loop_momentum_basis.clone()];
899
900        let mut parallel_edges_only: SuBitGraph = graph.empty_subgraph();
901        for active_loop_index in [LoopIndex(0), LoopIndex(1)] {
902            let active_loop_edge = graph.loop_momentum_basis.loop_edges[active_loop_index];
903            parallel_edges_only.union_with(&graph.get_edge_subgraph(active_loop_edge));
904        }
905        let parallel_edge_subspace = SubspaceData::new_with_user_selected_lmb(
906            parallel_edges_only,
907            LmbIndex::from(0),
908            &graph,
909            &all_lmbs,
910        )
911        .unwrap();
912        assert_eq!(
913            parallel_edge_subspace.loopcount(),
914            1,
915            "two graph-parallel defining edges without their spanning support contain only one independent loop"
916        );
917
918        let raised_graph: Graph = dot!(digraph raised_signature_subspace {
919            ext [style=invis]
920            edge [num=1 mass=0]
921            node [num=1]
922            ext->a [id=0]
923            a->b [id=1]
924            b->c [id=2]
925            c->a [id=3]
926            ext->c [id=4]
927        })
928        .unwrap();
929        let raised_group = raised_graph
930            .get_raised_edge_groups()
931            .into_iter()
932            .find(|group| group.len() >= 2)
933            .expect("test graph must contain a raised equal-signature edge group");
934        assert!(raised_group.iter().tuple_windows().all(|(left, right)| {
935            raised_graph.loop_momentum_basis.edge_signatures[*left]
936                .equality_up_to_sign(&raised_graph.loop_momentum_basis.edge_signatures[*right])
937        }));
938        let mut raised_subgraph: SuBitGraph = raised_graph.empty_subgraph();
939        for &edge in &raised_group {
940            raised_subgraph.union_with(&raised_graph.get_edge_subgraph(edge));
941        }
942        let raised_lmbs = ti_vec![raised_graph.loop_momentum_basis.clone()];
943        let raised_edges_only = SubspaceData::new_with_user_selected_lmb(
944            raised_subgraph.clone(),
945            LmbIndex::from(0),
946            &raised_graph,
947            &raised_lmbs,
948        )
949        .unwrap();
950        assert_eq!(
951            raised_edges_only.loopcount(),
952            0,
953            "a chain of raised equal-signature edges is not multiple independent loops"
954        );
955
956        let support_edge = raised_graph
957            .iter_loop_edges()
958            .map(|(_, edge, _)| edge)
959            .find(|edge| !raised_group.contains(edge))
960            .expect("test graph must contain the support edge closing the loop");
961        raised_subgraph.union_with(&raised_graph.get_edge_subgraph(support_edge));
962        let raised_cycle = SubspaceData::new_with_user_selected_lmb(
963            raised_subgraph,
964            LmbIndex::from(0),
965            &raised_graph,
966            &raised_lmbs,
967        )
968        .unwrap();
969        assert_eq!(
970            raised_cycle.loopcount(),
971            1,
972            "raised equal-signature edges must be counted by topology, not once per edge"
973        );
974
975        // Include the parent LMB spanning-tree support. Two graph-parallel
976        // defining edges alone contain only one independent cycle.
977        let mut subgraph = graph.loop_momentum_basis.tree.clone();
978        for active_loop_index in [LoopIndex(0), LoopIndex(1)] {
979            let active_loop_edge = graph.loop_momentum_basis.loop_edges[active_loop_index];
980            subgraph.union_with(&graph.get_edge_subgraph(active_loop_edge));
981        }
982        let subspace = SubspaceData::new_with_user_selected_lmb(
983            subgraph.clone(),
984            LmbIndex::from(0),
985            &graph,
986            &all_lmbs,
987        )
988        .unwrap();
989        assert_eq!(subspace.loopcount(), 2);
990
991        let radial_surface = Esurface {
992            energies: vec![graph.loop_momentum_basis.loop_edges[LoopIndex(0)]],
993            external_shift: vec![(EdgeIndex::from(0), -1)],
994            vertex_set: VertexSet::dummy(),
995        };
996        assert!(radial_surface.has_radial_dependence_in_subspace(&subspace, &all_lmbs, &graph,));
997        let zero_loop_momenta = LoopMomenta::from_iter([
998            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
999            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1000            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1001        ]);
1002        let classify_radial_surface = |energy| {
1003            let external_momenta = ExternalFourMomenta::from_iter([
1004                FourMomentum::from_args(F(energy), F(10.0), F(0.0), F(0.0)),
1005                FourMomentum::from_args(F(-energy), F(-10.0), F(0.0), F(0.0)),
1006            ]);
1007            radial_surface.classify_existence_subspace(
1008                &zero_loop_momenta,
1009                &external_momenta,
1010                &subspace,
1011                &all_lmbs,
1012                &graph,
1013                &graph.underlying.new_edgevec(|_, _, _| F(0.0)),
1014                &[],
1015                &F(10.0),
1016                &F(crate::utils::DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1017            )
1018        };
1019        assert!(matches!(
1020            classify_radial_surface(11.0),
1021            EsurfaceExistence::Existing { .. }
1022        ));
1023        assert!(matches!(
1024            classify_radial_surface(10.0),
1025            EsurfaceExistence::Pinched { .. }
1026        ));
1027        assert!(matches!(
1028            classify_radial_surface(9.0),
1029            EsurfaceExistence::NonExisting { .. }
1030        ));
1031
1032        let complement_edge = graph.loop_momentum_basis.loop_edges[LoopIndex(2)];
1033        let thresholds: crate::cff::esurface::EsurfaceCollection = vec![Esurface {
1034            energies: vec![complement_edge],
1035            external_shift: vec![(EdgeIndex::from(0), -1)],
1036            vertex_set: VertexSet::dummy(),
1037        }]
1038        .into();
1039        assert!(
1040            !thresholds[EsurfaceID::from(0)]
1041                .has_radial_dependence_in_subspace(&subspace, &all_lmbs, &graph,)
1042        );
1043        let masses = graph.underlying.new_edgevec(|_, _, _| F(0.0));
1044        let settings = RuntimeSettings::default();
1045        let overlap_input = OverlapInput {
1046            graph: &graph,
1047            settings: &settings,
1048            subspace: &subspace,
1049            lmbs: &all_lmbs,
1050            thresholds: &thresholds,
1051            edge_masses: masses,
1052        };
1053
1054        let external_momenta = ExternalFourMomenta::from_iter([
1055            FourMomentum::from_args(F(1.0), F(0.0), F(0.0), F(0.0)),
1056            FourMomentum::from_args(F(-1.0), F(0.0), F(0.0), F(0.0)),
1057        ]);
1058        let center = LoopMomenta::from_iter([
1059            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1060            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1061            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1062        ]);
1063        let sampled_momenta = LoopMomenta::from_iter([
1064            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1065            ThreeMomentum::new(F(0.0), F(0.0), F(0.0)),
1066            ThreeMomentum::new(F(2.0), F(0.0), F(0.0)),
1067        ]);
1068
1069        let origin_value = overlap_input.thresholds[EsurfaceID::from(0)].compute_from_momenta(
1070            subspace.get_lmb(&all_lmbs),
1071            &overlap_input.edge_masses,
1072            &center,
1073            &external_momenta,
1074        );
1075        assert!(origin_value < F(0.0));
1076        assert!(!check_global_center(
1077            &overlap_input,
1078            &ti_vec![EsurfaceID::from(0)],
1079            &center,
1080            &sampled_momenta,
1081            &external_momenta,
1082        ));
1083        assert!(check_global_center(
1084            &overlap_input,
1085            &ti_vec![EsurfaceID::from(0)],
1086            &center,
1087            &center,
1088            &external_momenta,
1089        ));
1090
1091        let forced_center_coordinates = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1092        let identity_frame_center =
1093            LoopMomenta::from_iter(forced_center_coordinates.iter().map(|coordinates| {
1094                ThreeMomentum::new(F(coordinates[0]), F(coordinates[1]), F(coordinates[2]))
1095            }));
1096        let probe_rotation = Rotation::new(RotationMethod::Pi2Z);
1097        let expected_probe_center = identity_frame_center.rotate(&probe_rotation);
1098        let mut expected_subspace_probe_center = expected_probe_center.clone();
1099        for loop_index in (0..expected_subspace_probe_center.len()).map(LoopIndex::from) {
1100            if !subspace.contains_loop_index(loop_index) {
1101                expected_subspace_probe_center[loop_index] =
1102                    ThreeMomentum::new(F(0.0), F(0.0), F(0.0));
1103            }
1104        }
1105        let mut forced_settings = RuntimeSettings::default();
1106        forced_settings
1107            .subtraction
1108            .overlap_settings
1109            .force_global_center = Some(forced_center_coordinates.clone());
1110        let forced_overlap_input = OverlapInput {
1111            graph: &graph,
1112            settings: &forced_settings,
1113            subspace: &subspace,
1114            lmbs: &all_lmbs,
1115            thresholds: &thresholds,
1116            edge_masses: overlap_input.edge_masses.clone(),
1117        };
1118
1119        let forced_overlap = find_maximal_overlap(
1120            &forced_overlap_input,
1121            &ti_vec![EsurfaceID::from(0)],
1122            &center,
1123            &external_momenta,
1124            &probe_rotation,
1125        )
1126        .unwrap();
1127        assert_eq!(forced_overlap.overlap_groups.len(), 1);
1128        assert_eq!(
1129            forced_overlap.overlap_groups[0].center, expected_subspace_probe_center,
1130            "an identity-frame forced center must be rotated exactly once and projected onto the active subspace"
1131        );
1132
1133        let alternate_lmb = graph
1134            .generate_loop_momentum_bases()
1135            .into_iter()
1136            .find(|lmb| lmb.loop_edges != graph.loop_momentum_basis.loop_edges)
1137            .expect("test graph must admit a non-default parent LMB");
1138        let alternate_lmbs = ti_vec![graph.loop_momentum_basis.clone(), alternate_lmb];
1139        let full_subspace = SubspaceData::new_with_user_selected_lmb(
1140            graph.full_filter(),
1141            LmbIndex::from(1),
1142            &graph,
1143            &alternate_lmbs,
1144        )
1145        .unwrap();
1146        let empty_thresholds: EsurfaceCollection = Vec::new().into();
1147        let alternate_overlap_input = OverlapInput {
1148            graph: &graph,
1149            settings: &forced_settings,
1150            subspace: &full_subspace,
1151            lmbs: &alternate_lmbs,
1152            thresholds: &empty_thresholds,
1153            edge_masses: overlap_input.edge_masses.clone(),
1154        };
1155        let rotated_external_spatial = external_momenta
1156            .iter()
1157            .map(|momentum| momentum.spatial)
1158            .collect();
1159        let expected_alternate_lmb_center = expected_probe_center.lmb_transform(
1160            &graph.loop_momentum_basis,
1161            full_subspace.get_lmb(&alternate_lmbs),
1162            &rotated_external_spatial,
1163        );
1164        let alternate_overlap = find_maximal_overlap(
1165            &alternate_overlap_input,
1166            &ti_vec![],
1167            &center,
1168            &external_momenta,
1169            &probe_rotation,
1170        )
1171        .unwrap();
1172        assert_eq!(
1173            alternate_overlap.overlap_groups[0].center, expected_alternate_lmb_center,
1174            "a forced center must be transformed from the graph LMB into the selected parent LMB after its single probe rotation"
1175        );
1176
1177        let proper_alternate_subspace = SubspaceData::new_with_user_selected_lmb(
1178            subgraph,
1179            LmbIndex::from(1),
1180            &graph,
1181            &alternate_lmbs,
1182        )
1183        .expect("the non-default parent LMB must support the same proper two-loop subspace");
1184        assert_eq!(proper_alternate_subspace.loopcount(), 2);
1185        let affine_external_momenta = ExternalFourMomenta::from_iter([
1186            FourMomentum::from_args(F(5.0), F(1.0), F(2.0), F(3.0)),
1187            FourMomentum::from_args(F(-5.0), F(-1.0), F(-2.0), F(-3.0)),
1188        ]);
1189        let affine_external_spatial = affine_external_momenta
1190            .iter()
1191            .map(|momentum| momentum.spatial)
1192            .collect();
1193        let unprojected_affine_center = identity_frame_center.lmb_transform(
1194            &graph.loop_momentum_basis,
1195            proper_alternate_subspace.get_lmb(&alternate_lmbs),
1196            &affine_external_spatial,
1197        );
1198        assert!(
1199            unprojected_affine_center
1200                .iter_enumerated()
1201                .any(|(loop_index, momentum)| {
1202                    !proper_alternate_subspace.contains_loop_index(loop_index)
1203                        && momentum.norm_squared() > F(0.0)
1204                }),
1205            "the fixture must exercise an affine LMB transform with a nonzero inactive component"
1206        );
1207        let mut expected_projected_affine_center = unprojected_affine_center.clone();
1208        for loop_index in (0..expected_projected_affine_center.len()).map(LoopIndex::from) {
1209            if !proper_alternate_subspace.contains_loop_index(loop_index) {
1210                expected_projected_affine_center[loop_index] =
1211                    ThreeMomentum::new(F(0.0), F(0.0), F(0.0));
1212            }
1213        }
1214        let mut affine_forced_settings = RuntimeSettings::default();
1215        affine_forced_settings
1216            .subtraction
1217            .overlap_settings
1218            .force_global_center = Some(forced_center_coordinates);
1219        let affine_overlap_input = OverlapInput {
1220            graph: &graph,
1221            settings: &affine_forced_settings,
1222            subspace: &proper_alternate_subspace,
1223            lmbs: &alternate_lmbs,
1224            thresholds: &empty_thresholds,
1225            edge_masses: overlap_input.edge_masses.clone(),
1226        };
1227        let affine_overlap = find_maximal_overlap(
1228            &affine_overlap_input,
1229            &ti_vec![],
1230            &sampled_momenta,
1231            &affine_external_momenta,
1232            &Rotation::new(RotationMethod::Identity),
1233        )
1234        .unwrap();
1235        assert_eq!(
1236            affine_overlap.overlap_groups[0].center, expected_projected_affine_center,
1237            "an affine parent-LMB transform must not displace the fixed complement of a forced subspace center"
1238        );
1239
1240        let support_edge = [1, 2, 3, 4]
1241            .into_iter()
1242            .map(EdgeIndex::from)
1243            .find(|edge| !graph.loop_momentum_basis.loop_edges.contains(edge))
1244            .unwrap();
1245        let covariant_thresholds = vec![Esurface {
1246            energies: vec![
1247                graph.loop_momentum_basis.loop_edges[LoopIndex(0)],
1248                support_edge,
1249            ],
1250            external_shift: vec![(EdgeIndex::from(0), -1)],
1251            vertex_set: VertexSet::dummy(),
1252        }]
1253        .into();
1254        let covariant_overlap_input = OverlapInput {
1255            graph: &graph,
1256            settings: &settings,
1257            subspace: &subspace,
1258            lmbs: &all_lmbs,
1259            thresholds: &covariant_thresholds,
1260            edge_masses: overlap_input.edge_masses.clone(),
1261        };
1262        let covariant_loop_momenta = LoopMomenta::from_iter([
1263            ThreeMomentum::new(F(0.3), F(-0.4), F(0.5)),
1264            ThreeMomentum::new(F(-0.2), F(0.7), F(0.1)),
1265            ThreeMomentum::new(F(0.6), F(-0.8), F(0.9)),
1266        ]);
1267        let covariant_externals = ExternalFourMomenta::from_iter([
1268            FourMomentum::from_args(F(20.0), F(6.0), F(8.0), F(0.0)),
1269            FourMomentum::from_args(F(-20.0), F(-6.0), F(-8.0), F(0.0)),
1270        ]);
1271        let existing = ti_vec![EsurfaceID::from(0)];
1272        let identity_center = find_center(
1273            &covariant_overlap_input,
1274            &[ExistingEsurfaceId::from(0)],
1275            &existing,
1276            &covariant_loop_momenta,
1277            &covariant_externals,
1278            false,
1279        )
1280        .unwrap();
1281        assert!(check_global_center(
1282            &covariant_overlap_input,
1283            &existing,
1284            &identity_center,
1285            &covariant_loop_momenta,
1286            &covariant_externals,
1287        ));
1288
1289        for rotation_method in [
1290            RotationMethod::Identity,
1291            RotationMethod::Pi2X,
1292            RotationMethod::Pi2Y,
1293            RotationMethod::Pi2Z,
1294            RotationMethod::EulerAngles(0.1, 0.2, 0.3),
1295        ] {
1296            let stability_rotation = Rotation::new(rotation_method);
1297            let rotated_loop_momenta = covariant_loop_momenta.rotate(&stability_rotation);
1298            let rotated_externals = covariant_externals
1299                .iter()
1300                .map(|momentum| FourMomentum {
1301                    temporal: momentum.temporal,
1302                    spatial: momentum.spatial.rotate(&stability_rotation),
1303                })
1304                .collect();
1305            let rotated_center = find_center(
1306                &covariant_overlap_input,
1307                &[ExistingEsurfaceId::from(0)],
1308                &existing,
1309                &rotated_loop_momenta,
1310                &rotated_externals,
1311                false,
1312            )
1313            .unwrap();
1314            let expected_rotated_center = identity_center.rotate(&stability_rotation);
1315
1316            for (actual, expected) in rotated_center.iter().zip(expected_rotated_center.iter()) {
1317                for (actual_component, expected_component) in actual.into_iter().zip(expected) {
1318                    assert!(
1319                        (actual_component.0 - expected_component.0).abs() < 1.0e-8,
1320                        "solver-derived center is not covariant under stability rotation {rotation_method}: actual={actual_component}, expected={expected_component}"
1321                    );
1322                }
1323            }
1324            assert!(
1325                check_global_center(
1326                    &covariant_overlap_input,
1327                    &existing,
1328                    &rotated_center,
1329                    &rotated_loop_momenta,
1330                    &rotated_externals,
1331                ),
1332                "solver-derived center is not strictly interior after stability rotation {rotation_method}",
1333            );
1334        }
1335    }
1336}