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 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}
97struct PropagatorConstraint<'a> {
99 mass_pointer: Option<usize>, 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 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 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 let p_matrix: CscMatrix<f64> =
223 CscMatrix::spalloc((num_primal_variables, num_primal_variables), 0);
224
225 let mut q_vector = vec![0.0; num_primal_variables];
227 q_vector[0] = 1.0;
228
229 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 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 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 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 ¢er,
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 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 ¢er,
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 ¢er_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
436pub(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 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 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 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 let esurface_pairs = EsurfacePairs::new(
586 overlap_input,
587 existing_esurfaces,
588 loop_moms,
589 external_momenta,
590 );
591
592 let mut num_disconnected_surfaces = 0;
597
598 for (existing_esurface_id, &esurface_id) in existing_esurfaces.iter_enumerated() {
599 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 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 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 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 ¢er,
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 ¢er,
1080 &sampled_momenta,
1081 &external_momenta,
1082 ));
1083 assert!(check_global_center(
1084 &overlap_input,
1085 &ti_vec![EsurfaceID::from(0)],
1086 ¢er,
1087 ¢er,
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 ¢er,
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 ¢er,
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}