1use std::fmt::Display;
2
3use bincode_trait_derive::{Decode, Encode};
4use derive_more::{From, Into};
5use eyre::eyre;
6use itertools::Itertools;
7use linnet::half_edge::HedgeGraph;
8use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Flow, HedgePair, Orientation};
9use linnet::half_edge::subgraph::{OrientedCut, SuBitGraph, SubSetLike, SubSetOps};
10use ref_ops::RefNeg;
11use serde::{Deserialize, Serialize};
12
13use symbolica::atom::{Atom, AtomCore};
14use symbolica::domains::dual::HyperDual;
15use symbolica::domains::float::{FloatLike as SymFloatLike, Real};
16use symbolica::id::Replacement;
17use symbolica::{function, parse};
18use tracing::debug;
19use typed_index_collections::TiVec;
20
21use crate::cff::cff_graph::VertexSet;
22
23use crate::cff::expression::{CFFExpression, OrientationID};
24use crate::graph::{Graph, GraphGroupPosition, LmbIndex, LoopMomentumBasis};
25use crate::{GammaLoopContext, define_index};
26
27use crate::integrands::process::GenericEvaluator;
28use crate::momentum::sample::{
29 ExternalFourMomenta, ExternalIndex, ExternalThreeMomenta, LoopIndex, LoopMomenta, SubspaceData,
30};
31use crate::momentum::{SignOrZero, ThreeMomentum};
32use crate::processes::CrossSectionCut;
33use crate::utils::hyperdual_utils::new_constant;
34use crate::utils::{
35 DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, ESURFACE_SHIFT_THRESHOLD, F, FloatLike, GS,
36 compute_loop_part, compute_loop_part_subspace, compute_shift_part, compute_shift_part_subspace,
37 compute_t_part_of_shift_part, cut_energy, external_energy_atom_from_index, ose_atom_from_index,
38};
39use crate::uv::uv_graph::UVE;
40use color_eyre::Result;
41
42use super::generation::ShiftRewrite;
43
44#[derive(Serialize, Deserialize, Debug, Clone, bincode::Encode, bincode::Decode)]
46pub struct Esurface {
47 pub energies: Vec<EdgeIndex>,
48 pub external_shift: ExternalShift,
49 pub vertex_set: VertexSet,
50 }
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub(crate) enum NonExistingEsurfaceReason {
56 NoExternalShift,
57 ShiftNotNegative,
58 NoRadialDependence,
59 NoRealZero,
60}
61
62#[derive(Debug, Clone)]
63pub(crate) enum EsurfaceExistence<T: FloatLike> {
64 NonExisting {
65 normalized_margin: Option<F<T>>,
66 reason: NonExistingEsurfaceReason,
67 },
68 Pinched {
69 normalized_margin: F<T>,
70 },
71 Existing {
72 normalized_margin: F<T>,
73 },
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum EsurfaceExistenceStatus {
79 NonExisting,
80 Pinched,
81 Existing,
82}
83
84impl<T: FloatLike> EsurfaceExistence<T> {
85 fn status(&self) -> EsurfaceExistenceStatus {
86 match self {
87 Self::NonExisting { .. } => EsurfaceExistenceStatus::NonExisting,
88 Self::Pinched { .. } => EsurfaceExistenceStatus::Pinched,
89 Self::Existing { .. } => EsurfaceExistenceStatus::Existing,
90 }
91 }
92
93 pub(crate) fn is_existing(&self) -> bool {
94 matches!(self, Self::Existing { .. })
95 }
96
97 pub(crate) fn normalized_margin(&self) -> Option<&F<T>> {
98 match self {
99 Self::NonExisting {
100 normalized_margin, ..
101 } => normalized_margin.as_ref(),
102 Self::Pinched { normalized_margin } | Self::Existing { normalized_margin } => {
103 Some(normalized_margin)
104 }
105 }
106 }
107
108 pub(crate) fn label(&self) -> &'static str {
109 match self {
110 Self::NonExisting { .. } => "non_existing",
111 Self::Pinched { .. } => "pinched",
112 Self::Existing { .. } => "existing",
113 }
114 }
115
116 pub(crate) fn non_existing_reason(&self) -> Option<NonExistingEsurfaceReason> {
117 match self {
118 Self::NonExisting { reason, .. } => Some(*reason),
119 Self::Pinched { .. } | Self::Existing { .. } => None,
120 }
121 }
122}
123
124pub(crate) fn esurface_value_is_strictly_inside<T: FloatLike>(value: &F<T>, e_cm: &F<T>) -> bool {
125 let interior_tolerance = value.epsilon() * value.from_i64(8) * e_cm;
126 !value.is_nan() && !value.is_infinite() && value < &(-interior_tolerance)
127}
128
129impl PartialEq for Esurface {
130 fn eq(&self, other: &Self) -> bool {
131 self.energies == other.energies && self.external_shift == other.external_shift
132 }
133}
134
135impl Eq for Esurface {}
136
137impl Esurface {
138 pub(crate) fn has_radial_dependence_in_subspace(
139 &self,
140 subspace: &SubspaceData,
141 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
142 graph: &Graph,
143 ) -> bool {
144 let lmb = subspace.get_lmb(all_lmbs);
145 subspace.contains(&self.energies, graph).any(|index| {
146 subspace
147 .project_loop_signature(&lmb.edge_signatures[index].internal)
148 .any(|sign| sign.is_sign())
149 })
150 }
151
152 pub(crate) fn external_shift_is_strictly_negative_for_positive_energies(
153 &self,
154 incoming_edges: &[EdgeIndex],
155 outgoing_edges: &[EdgeIndex],
156 ) -> bool {
157 if incoming_edges.is_empty()
158 || outgoing_edges.is_empty()
159 || incoming_edges
160 .iter()
161 .any(|edge| outgoing_edges.contains(edge))
162 || self
163 .external_shift
164 .iter()
165 .any(|(edge, _)| !incoming_edges.contains(edge) && !outgoing_edges.contains(edge))
166 {
167 return false;
168 }
169
170 let coefficient = |edge: &EdgeIndex| {
171 self.external_shift
172 .iter()
173 .filter(|(shift_edge, _)| shift_edge == edge)
174 .map(|(_, coefficient)| i128::from(*coefficient))
175 .sum::<i128>()
176 };
177
178 let lambda_lower_bound = outgoing_edges
184 .iter()
185 .map(&coefficient)
186 .max()
187 .expect("outgoing external edges were checked to be non-empty");
188 let lambda_upper_bound = incoming_edges
189 .iter()
190 .map(|edge| -coefficient(edge))
191 .min()
192 .expect("incoming external edges were checked to be non-empty");
193
194 if lambda_lower_bound > lambda_upper_bound {
195 return false;
196 }
197
198 let lambda = lambda_lower_bound;
199 let adjusted_coefficients = incoming_edges
200 .iter()
201 .map(|edge| coefficient(edge) + lambda)
202 .chain(outgoing_edges.iter().map(|edge| coefficient(edge) - lambda));
203 let mut has_strictly_negative_coefficient = false;
204 for adjusted_coefficient in adjusted_coefficients {
205 if adjusted_coefficient > 0 {
206 return false;
207 }
208 has_strictly_negative_coefficient |= adjusted_coefficient < 0;
209 }
210
211 has_strictly_negative_coefficient
212 }
213
214 pub(crate) fn to_atom(&self, cut_edges: &[EdgeIndex]) -> Atom {
215 self.to_atom_impl(cut_edges, external_energy_atom_from_index)
216 }
217
218 pub(crate) fn to_atom_in_lmb(&self, cut_edges: &[EdgeIndex], lmb: &LoopMomentumBasis) -> Atom {
219 self.to_atom_impl(cut_edges, |edge| {
220 lmb.edge_signatures[edge].external.iter_enumerated().fold(
221 Atom::Zero,
222 |sum, (external_index, sign)| {
223 let atom = external_energy_atom_from_index(lmb.ext_edges[external_index]);
224 match sign {
225 SignOrZero::Zero => sum,
226 SignOrZero::Plus => sum + atom,
227 SignOrZero::Minus => sum - atom,
228 }
229 },
230 )
231 })
232 }
233
234 fn to_atom_impl(
235 &self,
236 cut_edges: &[EdgeIndex],
237 external_shift_atom: impl Fn(EdgeIndex) -> Atom,
238 ) -> Atom {
239 let symbolic_energies = self
240 .energies
241 .iter()
242 .map(|i| {
243 if cut_edges.contains(i) {
244 cut_energy(*i)
245 } else {
246 ose_atom_from_index(*i)
247 }
248 })
249 .collect_vec();
250
251 let symbolic_shift = self
252 .external_shift
253 .iter()
254 .fold(Atom::new(), |sum, (i, sign)| {
255 external_shift_atom(*i) * &Atom::num(*sign) + &sum
256 });
257
258 let builder_atom = Atom::new();
259 let energy_sum = symbolic_energies
260 .iter()
261 .fold(builder_atom, |acc, energy| acc + energy);
262
263 energy_sum + &symbolic_shift
264 }
265
266 #[inline]
267 pub(crate) fn compute_from_dual_momenta<T: FloatLike>(
268 &self,
269 lmb: &LoopMomentumBasis,
270 real_mass_vector: &EdgeVec<F<T>>,
271 dual_loop_moms: &LoopMomenta<HyperDual<F<T>>>,
272 dual_external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
273 ) -> HyperDual<F<T>> {
274 let spatial_part_of_externals = dual_external_moms
275 .iter()
276 .map(|mom| mom.spatial.clone())
277 .collect::<TiVec<ExternalIndex, _>>();
278
279 let energy_sum = self
280 .energies
281 .iter()
282 .map(|index| {
283 let signature = &lmb.edge_signatures[*index];
284 let momentum = signature
285 .try_compute_momentum(&dual_loop_moms.0, &spatial_part_of_externals.raw)
286 .unwrap_or_else(|| unreachable!());
287 let mass = &real_mass_vector[*index];
288
289 (momentum.norm_squared() + mass * mass).sqrt()
290 })
291 .reduce(|acc, x| acc + x)
292 .unwrap_or_else(|| dual_loop_moms[LoopIndex(0)].px.zero());
293
294 let shift_part = self
295 .external_shift
296 .iter()
297 .map(|(index, sign)| {
298 let external_signature = &lmb.edge_signatures[*index].external;
299 new_constant(&energy_sum, &F::from_f64(*sign as f64))
300 * external_signature
301 .try_apply(&dual_external_moms.raw)
302 .map(|mom| mom.temporal.value)
303 .unwrap_or_else(|| energy_sum.zero())
304 })
305 .reduce(|acc, x| acc + x)
306 .unwrap_or_else(|| energy_sum.zero());
307
308 energy_sum + shift_part
309 }
310
311 #[inline]
314 pub(crate) fn compute_from_momenta<T: FloatLike>(
315 &self,
316 lmb: &LoopMomentumBasis,
317 real_mass_vector: &EdgeVec<F<T>>,
318 loop_moms: &LoopMomenta<F<T>>,
319 external_moms: &ExternalFourMomenta<F<T>>,
320 ) -> F<T> {
321 let spatial_part_of_externals = external_moms
322 .iter()
323 .map(|mom| mom.spatial.clone())
324 .collect::<TiVec<ExternalIndex, _>>();
325
326 let energy_sum = self
327 .energies
328 .iter()
329 .map(|index| {
330 let signature = &lmb.edge_signatures[*index];
331 let momentum = signature.compute_momentum(loop_moms, &spatial_part_of_externals);
332 let mass = &real_mass_vector[*index];
333
334 (momentum.norm_squared() + mass * mass).sqrt()
335 })
336 .reduce(|acc, x| acc + x)
337 .unwrap_or_else(|| loop_moms[LoopIndex(0)].px.zero());
338
339 let shift_part = self
340 .external_shift
341 .iter()
342 .map(|(index, sign)| {
343 let external_signature = &lmb.edge_signatures[*index].external;
344 F::from_f64(*sign as f64)
345 * compute_t_part_of_shift_part(external_signature, external_moms)
346 })
347 .reduce(|acc, x| acc + x)
348 .unwrap_or_else(|| energy_sum.zero());
349
350 energy_sum + shift_part
351 }
352
353 fn classify_invariant_margin<T: FloatLike>(
354 shift_part: &F<T>,
355 invariant_margin: F<T>,
356 e_cm: &F<T>,
357 normalized_margin_tolerance: &F<T>,
358 ) -> EsurfaceExistence<T> {
359 let normalized_margin_tolerance =
372 if normalized_margin_tolerance.is_nan() || normalized_margin_tolerance.is_infinite() {
373 F::from_f64(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD)
374 } else {
375 normalized_margin_tolerance.abs()
376 };
377 let invariant_tolerance = &normalized_margin_tolerance * e_cm * e_cm;
378 let normalized_margin = &invariant_margin / (e_cm * e_cm);
379 let shift_tolerance = F::from_f64(ESURFACE_SHIFT_THRESHOLD) * e_cm;
380
381 if invariant_margin.abs() <= invariant_tolerance && shift_part <= &shift_tolerance {
382 return EsurfaceExistence::Pinched { normalized_margin };
383 }
384
385 if shift_part >= &(-shift_tolerance) {
386 return EsurfaceExistence::NonExisting {
387 normalized_margin: Some(normalized_margin),
388 reason: NonExistingEsurfaceReason::ShiftNotNegative,
389 };
390 }
391
392 if invariant_margin > invariant_tolerance {
393 EsurfaceExistence::Existing { normalized_margin }
394 } else if invariant_margin >= -invariant_tolerance {
395 EsurfaceExistence::Pinched { normalized_margin }
396 } else {
397 EsurfaceExistence::NonExisting {
398 normalized_margin: Some(normalized_margin),
399 reason: NonExistingEsurfaceReason::NoRealZero,
400 }
401 }
402 }
403
404 #[inline]
405 #[allow(clippy::too_many_arguments)]
406 pub(crate) fn classify_existence_subspace<T: FloatLike>(
409 &self,
410 loop_moms: &LoopMomenta<F<T>>,
411 external_moms: &ExternalFourMomenta<F<T>>,
412 subspace: &SubspaceData,
413 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
414 graph: &Graph,
415 real_mass_vector: &EdgeVec<F<T>>,
416 reversed_edges: &[EdgeIndex],
417 e_cm: &F<T>,
418 normalized_margin_tolerance: &F<T>,
419 ) -> EsurfaceExistence<T> {
420 if self.external_shift.is_empty() {
422 debug!("esurface has no external shift, cannot exist");
423 return EsurfaceExistence::NonExisting {
424 normalized_margin: None,
425 reason: NonExistingEsurfaceReason::NoExternalShift,
426 };
427 }
428
429 let shift_part = self.compute_shift_part_from_momenta_in_subspace(
430 loop_moms,
431 external_moms,
432 subspace,
433 all_lmbs,
434 graph,
435 real_mass_vector,
436 );
437
438 let subspace_energy_indices = subspace.contains(&self.energies, graph).collect_vec();
439
440 if !self.has_radial_dependence_in_subspace(subspace, all_lmbs, graph) {
441 debug!(
442 "esurface has no radial energy in this subspace, cannot bound a threshold region"
443 );
444 return EsurfaceExistence::NonExisting {
445 normalized_margin: None,
446 reason: NonExistingEsurfaceReason::NoRadialDependence,
447 };
448 }
449
450 let lmb = subspace.get_lmb(all_lmbs);
451 let mass_sum: F<T> = subspace_energy_indices
452 .iter()
453 .map(|&index| &real_mass_vector[index])
454 .fold(F::from_f64(0.0), |acc, x| acc + x);
455
456 let zero_vector = ThreeMomentum::new(e_cm.zero(), e_cm.zero(), e_cm.zero());
457
458 let graph_vector = self
459 .external_shift
460 .iter()
461 .map(|(index, sign)| {
462 let external_signature = &lmb.edge_signatures[*index].external;
463 compute_shift_part(external_signature, external_moms).spatial
464 * F::from_f64(*sign as f64)
465 })
466 .reduce(|acc, x| acc + x)
467 .unwrap_or_else(|| zero_vector.clone());
468
469 let other_part = subspace
470 .does_not_contain(&self.energies, graph)
471 .map(|index| {
472 let signature = &lmb.edge_signatures[index];
473 let sign = if reversed_edges.contains(&index) {
474 -F::from_f64(1.0)
475 } else {
476 F::from_f64(1.0)
477 };
478
479 signature.compute_momentum(
480 loop_moms,
481 &external_moms
482 .iter()
483 .map(|mom| mom.spatial.clone())
484 .collect::<TiVec<ExternalIndex, _>>(),
485 ) * sign
486 })
487 .reduce(|acc, x| acc + x)
488 .unwrap_or_else(|| zero_vector.clone());
489
490 let shift_vector_sq = (&graph_vector + &other_part).norm_squared();
491 let invariant_margin = &shift_part * &shift_part - &shift_vector_sq - &mass_sum * &mass_sum;
492 let classification = Self::classify_invariant_margin(
493 &shift_part,
494 invariant_margin,
495 e_cm,
496 normalized_margin_tolerance,
497 );
498
499 if !classification.is_existing() {
500 debug!(
501 "subspace esurface classified as {}: shift_part^2: {}, shift_vector_sq: {}, mass_sum^2: {}, normalized_margin: {:?}",
502 classification.label(),
503 &shift_part * &shift_part,
504 shift_vector_sq,
505 &mass_sum * &mass_sum,
506 classification.normalized_margin(),
507 );
508 }
509
510 classification
511 }
512
513 #[inline]
514 pub(crate) fn classify_existence<T: FloatLike>(
517 &self,
518 external_moms: &ExternalFourMomenta<F<T>>,
519 lmb: &LoopMomentumBasis,
520 real_mass_vector: &EdgeVec<F<T>>,
521 e_cm: &F<T>,
522 normalized_margin_tolerance: &F<T>,
523 ) -> EsurfaceExistence<T> {
524 if self.external_shift.is_empty() {
526 return EsurfaceExistence::NonExisting {
527 normalized_margin: None,
528 reason: NonExistingEsurfaceReason::NoExternalShift,
529 };
530 }
531
532 let shift_part = self.compute_shift_part_from_momenta(external_moms, lmb);
533 let mass_sum: F<T> = self
534 .energies
535 .iter()
536 .map(|index| &real_mass_vector[*index])
537 .fold(F::from_f64(0.0), |acc, x| acc + x);
538
539 let zero_vector = ThreeMomentum::new(e_cm.zero(), e_cm.zero(), e_cm.zero());
540
541 let shift_vector = self
542 .external_shift
543 .iter()
544 .map(|(index, sign)| {
545 let external_signature = &lmb.edge_signatures[*index].external;
546 compute_shift_part(external_signature, external_moms).spatial
547 * F::from_f64(*sign as f64)
548 })
549 .reduce(|acc, x| acc + x)
550 .unwrap_or_else(|| zero_vector.clone());
551
552 let shift_vector_sq = shift_vector.norm_squared();
553 let invariant_margin = &shift_part * &shift_part - shift_vector_sq - &mass_sum * &mass_sum;
554
555 Self::classify_invariant_margin(
556 &shift_part,
557 invariant_margin,
558 e_cm,
559 normalized_margin_tolerance,
560 )
561 }
562
563 pub fn existence_status<T: FloatLike>(
566 &self,
567 external_moms: &ExternalFourMomenta<F<T>>,
568 lmb: &LoopMomentumBasis,
569 real_mass_vector: &EdgeVec<F<T>>,
570 e_cm: &F<T>,
571 normalized_margin_tolerance: &F<T>,
572 ) -> EsurfaceExistenceStatus {
573 self.classify_existence(
574 external_moms,
575 lmb,
576 real_mass_vector,
577 e_cm,
578 normalized_margin_tolerance,
579 )
580 .status()
581 }
582
583 pub(crate) fn compute_shift_part_from_momenta_in_subspace<T: FloatLike>(
585 &self,
586 loop_moms: &LoopMomenta<F<T>>,
587 external_moms: &ExternalFourMomenta<F<T>>,
588 subspace: &SubspaceData,
589 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
590 graph: &Graph,
591 masses: &EdgeVec<F<T>>,
592 ) -> F<T> {
593 let lmb = subspace.get_lmb(all_lmbs);
594
595 let full_external_shift = self
596 .external_shift
597 .iter()
598 .map(|(index, sign)| {
599 let external_signature = &lmb.edge_signatures[*index].external;
600 F::from_f64(*sign as f64)
601 * compute_t_part_of_shift_part(external_signature, external_moms)
602 })
603 .reduce(|acc, x| acc + x)
604 .unwrap_or_else(|| external_moms[ExternalIndex(0)].temporal.value.zero());
605
606 let spatial_externals = external_moms
607 .iter()
608 .map(|mom| mom.spatial.clone())
609 .collect::<TiVec<ExternalIndex, _>>();
610
611 let remaining_shift = subspace
612 .does_not_contain(&self.energies, graph)
613 .map(|index| {
614 let signature = &lmb.edge_signatures[index];
615 let momentum = signature.compute_momentum(loop_moms, &spatial_externals);
617 let mass = &masses[index];
618
619 (momentum.norm_squared() + mass * mass).sqrt()
620 })
621 .reduce(|acc, x| acc + x)
622 .unwrap_or_else(|| full_external_shift.zero());
623
624 full_external_shift + remaining_shift
625 }
626
627 pub(crate) fn compute_shift_part_from_momenta<T: FloatLike>(
629 &self,
630 external_moms: &ExternalFourMomenta<F<T>>,
631 lmb: &LoopMomentumBasis,
632 ) -> F<T> {
633 self.external_shift
634 .iter()
635 .map(|(index, sign)| {
636 let external_signature = &lmb.edge_signatures[*index].external;
637 F::from_f64(*sign as f64)
638 * compute_t_part_of_shift_part(external_signature, external_moms)
639 })
640 .reduce(|acc, x| acc + x)
641 .unwrap_or_else(|| external_moms[ExternalIndex(0)].temporal.value.zero())
642 }
643
644 #[inline]
645 #[allow(clippy::too_many_arguments)]
646 pub(crate) fn compute_self_and_r_derivative_subspace<T: FloatLike>(
647 &self,
648 radius: &F<T>,
649 shifted_unit_loops_in_subspace: &LoopMomenta<F<T>>,
650 center_in_subspace: &LoopMomenta<F<T>>,
651 external_moms: &ExternalFourMomenta<F<T>>,
652 real_mass_vector: &EdgeVec<F<T>>,
653 subspace: &SubspaceData,
654 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
655 graph: &Graph,
656 ) -> (F<T>, F<T>) {
657 let spatial_part_of_externals: ExternalThreeMomenta<F<T>> = external_moms
658 .iter()
659 .map(|mom| mom.spatial.clone())
660 .collect();
661
662 let loops: LoopMomenta<F<T>> = shifted_unit_loops_in_subspace
663 .iter_enumerated()
664 .map(|(loop_index, shifted_unit_momenta)| {
665 if subspace.contains_loop_index(loop_index) {
666 shifted_unit_momenta * radius + ¢er_in_subspace[loop_index]
667 } else {
668 shifted_unit_momenta.clone()
669 }
670 })
671 .collect();
672
673 let shift = self.compute_shift_part_from_momenta_in_subspace(
674 shifted_unit_loops_in_subspace,
675 external_moms,
676 subspace,
677 all_lmbs,
678 graph,
679 real_mass_vector,
680 );
681
682 let lmb = subspace.get_lmb(all_lmbs);
683 let (derivative, energy_sum) = subspace
684 .contains(&self.energies, graph)
685 .map(|index| {
686 let signature = &lmb.edge_signatures[index];
687
688 let momentum = signature.compute_momentum(&loops, &spatial_part_of_externals);
689 let unit_loop_part = compute_loop_part_subspace(
690 &signature.internal,
691 shifted_unit_loops_in_subspace,
692 subspace,
693 );
694
695 let energy = (momentum.norm_squared()
696 + &real_mass_vector[index] * &real_mass_vector[index])
697 .sqrt();
698
699 let numerator = momentum * &unit_loop_part;
700
701 (numerator / &energy, energy)
702 })
703 .fold(
704 (radius.zero(), radius.zero()),
705 |(der_sum, en_sum), (der, en)| (der_sum + der, en_sum + en),
706 );
707
708 (energy_sum + shift, derivative)
709 }
710
711 #[inline]
712 pub(crate) fn compute_self_and_r_derivative<T: FloatLike>(
713 &self,
714 radius: &F<T>,
715 shifted_unit_loops: &LoopMomenta<F<T>>,
716 center: &LoopMomenta<F<T>>,
717 external_moms: &ExternalFourMomenta<F<T>>,
718 real_mass_vector: &EdgeVec<F<T>>,
719 lmb: &LoopMomentumBasis,
720 ) -> (F<T>, F<T>) {
721 let spatial_part_of_externals: ExternalThreeMomenta<F<T>> = external_moms
722 .iter()
723 .map(|mom| mom.spatial.clone())
724 .collect();
725
726 let loops: LoopMomenta<F<T>> = shifted_unit_loops
727 .iter()
728 .zip(center.iter())
729 .map(|(momentum, center)| momentum * radius + center)
730 .collect();
731
732 let shift = self.compute_shift_part_from_momenta(external_moms, lmb);
733
734 let (derivative, energy_sum) = self
735 .energies
736 .iter()
737 .map(|&index| {
738 let signature = &lmb.edge_signatures[index];
739
740 let momentum = signature.compute_momentum(&loops, &spatial_part_of_externals);
741 let unit_loop_part = compute_loop_part(&signature.internal, shifted_unit_loops);
742
743 let energy = (momentum.norm_squared()
744 + &real_mass_vector[index] * &real_mass_vector[index])
745 .sqrt();
746
747 let numerator = momentum * &unit_loop_part;
748
749 (numerator / &energy, energy)
750 })
751 .fold(
752 (radius.zero(), radius.zero()),
753 |(der_sum, en_sum), (der, en)| (der_sum + der, en_sum + en),
754 );
755
756 (energy_sum + shift, derivative)
757 }
758
759 pub(crate) fn get_radius_guess_subspace<T: FloatLike>(
762 &self,
763 loops_unit_in_subspace: &LoopMomenta<F<T>>,
764 external_moms: &ExternalFourMomenta<F<T>>,
765 subspace: &SubspaceData,
766 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
767 graph: &Graph,
768 masses: &EdgeVec<F<T>>,
769 ) -> (F<T>, F<T>) {
770 let const_builder = &loops_unit_in_subspace[LoopIndex(0)].px;
771
772 let esurface_shift = self.compute_shift_part_from_momenta_in_subspace(
773 loops_unit_in_subspace,
774 external_moms,
775 subspace,
776 all_lmbs,
777 graph,
778 masses,
779 );
780
781 debug!("shift part for radius guess: {}", esurface_shift);
782
783 let mut radius_guess = const_builder.zero();
784 let mut denominator = const_builder.zero();
785
786 let lmb = subspace.get_lmb(all_lmbs);
787
788 debug!("unit loops in subspace: {:?}", loops_unit_in_subspace);
789 let external_3_momenta = external_moms.iter().map(|x| x.spatial.clone()).collect();
790
791 for energy in subspace.contains(&self.energies, graph) {
793 debug!("computing contribution for energy {:?}", energy);
794 let signature = &lmb.edge_signatures[energy];
795 let unit_loop_part =
798 compute_loop_part_subspace(&signature.internal, loops_unit_in_subspace, subspace);
799 let three_shift = compute_shift_part_subspace(
802 &signature.internal,
803 &signature.external,
804 loops_unit_in_subspace,
805 &external_3_momenta,
806 subspace,
807 );
808 let norm_unit_loop_part_squared = unit_loop_part.norm_squared();
811 let loop_dot_shift = &unit_loop_part * three_shift;
812
813 debug!(
814 "norm unit loop part squared: {}",
815 norm_unit_loop_part_squared
816 );
817
818 radius_guess += loop_dot_shift.abs() / &norm_unit_loop_part_squared;
819 debug!("current radius guess: {}", radius_guess);
820 denominator += norm_unit_loop_part_squared.sqrt();
821 debug!("current denominator: {}", denominator);
822 }
823
824 radius_guess += esurface_shift.abs() / denominator;
825 debug!("final radius guess: {}", radius_guess);
826 let negative_radius = radius_guess.ref_neg();
827 (radius_guess, negative_radius)
828 }
829
830 pub(crate) fn get_radius_guess<T: FloatLike>(
831 &self,
832 unit_loops: &LoopMomenta<F<T>>,
833 external_moms: &ExternalFourMomenta<F<T>>,
834 lmb: &LoopMomentumBasis,
835 ) -> (F<T>, F<T>) {
836 let const_builder = &unit_loops[LoopIndex(0)].px;
837
838 let esurface_shift = self.compute_shift_part_from_momenta(external_moms, lmb);
839
840 let mut radius_guess = const_builder.zero();
841 let mut denominator = const_builder.zero();
842
843 for &energy in &self.energies {
845 let signature = &lmb.edge_signatures[energy];
847 let unit_loop_part = compute_loop_part(&signature.internal, unit_loops);
850 let three_shift = compute_shift_part(&signature.external, external_moms).spatial;
853 let norm_unit_loop_part_squared = unit_loop_part.norm_squared();
856 let loop_dot_shift = &unit_loop_part * three_shift;
857
858 radius_guess += loop_dot_shift.abs() / &norm_unit_loop_part_squared;
859 denominator += norm_unit_loop_part_squared.sqrt();
860 }
861
862 radius_guess += esurface_shift.abs() / denominator;
863 let negative_radius = radius_guess.ref_neg();
864 (radius_guess, negative_radius)
865 }
866
867 pub(crate) fn canonicalize_shift(&mut self, shift_rewrite: &ShiftRewrite) {
868 if let Some(dep_mom_pos) = self
869 .external_shift
870 .iter()
871 .position(|(index, _)| *index == shift_rewrite.dependent_momentum)
872 {
873 let (_, dep_mom_sign) = self.external_shift.remove(dep_mom_pos);
874
875 let external_shift = shift_rewrite
876 .dependent_momentum_expr
877 .iter()
878 .map(|(index, sign)| (*index, dep_mom_sign * sign))
879 .collect();
880
881 self.external_shift = add_external_shifts(&self.external_shift, &external_shift);
882 }
883 }
884
885 pub(crate) fn new_from_subgraph(
886 subgraph: &SuBitGraph,
887 graph: &Graph,
888 orientation: &EdgeVec<Orientation>,
889 ) -> Self {
890 if graph.initial_state_cut.is_empty() {
891 todo!("handle case for amplitudes")
892 }
893
894 let subgraph_without_is_cut = subgraph.subtract(
895 &graph
896 .initial_state_cut
897 .left
898 .union(&graph.initial_state_cut.right),
899 );
900
901 let mut unit_flow = None;
902
903 let vertex_set = graph
904 .iter_nodes_of(subgraph)
905 .map(|(node_id, _, _)| VertexSet::from_usize(node_id.into()))
906 .reduce(|acc, v| acc.join(&v))
907 .unwrap();
908
909 let virtual_boundary = graph
910 .iter_edges_of(&subgraph_without_is_cut)
911 .filter_map(|(pair, edge_id, _)| match pair {
912 HedgePair::Split { split, .. } => {
913 if let Some(common_flow) = unit_flow {
914 match orientation[edge_id] {
915 Orientation::Default => {
916 if common_flow != split {
917 panic!("inconsistent flow on virtual boundary, cannot construct esurface");
918 }
919 }
920 Orientation::Reversed => {
921 if common_flow != -split {
922 panic!("inconsistent flow on virtual boundary, cannot construct esurface");
923 }
924 }
925 Orientation::Undirected => (),
926 }
927 } else {
928 match orientation[edge_id] {
929 Orientation::Default => unit_flow = Some(split),
930 Orientation::Reversed => unit_flow = Some(-split),
931 Orientation::Undirected => (),
932 }
933 }
934 Some(edge_id)
935 }
936 _ => None,
937 }).sorted()
938 .collect_vec();
939
940 let flow = unit_flow.expect("no virtual boundary found, cannot construct esurface");
941
942 let is_cut_part_of_subgraph = subgraph.intersection(
943 &graph
944 .initial_state_cut
945 .left
946 .union(&graph.initial_state_cut.right),
947 );
948
949 let mut exernal_shift = Vec::new();
950
951 for (pair, edge_index, _) in graph.iter_edges_of(&is_cut_part_of_subgraph) {
952 let HedgePair::Split {
953 split: edge_flow, ..
954 } = pair
955 else {
956 continue;
957 };
958
959 let sign = if flow == edge_flow { 1 } else { -1 };
960 exernal_shift.push((edge_index, sign));
961 }
962
963 Self {
964 energies: virtual_boundary,
965 external_shift: exernal_shift,
966 vertex_set,
967 }
968 }
969
970 pub(crate) fn new_from_cut_left<E, V, H>(
971 graph: &HedgeGraph<E, V, H>,
972 cut: &CrossSectionCut,
973 initial_state_cut: Option<&OrientedCut>,
974 ) -> Self {
975 let edges = graph
976 .iter_edges_of(&cut.cut)
977 .map(|(_, id, _)| id)
978 .sorted()
979 .collect();
980
981 let external_shift = if let Some(is_cut) = initial_state_cut {
982 graph
983 .iter_edges_of(is_cut)
984 .map(|(_, edge_index, __)| (edge_index, -1))
985 .sorted_by(|a, b| a.0.cmp(&b.0))
986 .collect()
987 } else {
988 graph
989 .iter_edges_of(&cut.left)
990 .filter_map(|(hedge_pair, edge_index, _)| match hedge_pair {
991 HedgePair::Unpaired { flow, .. } => match flow {
992 Flow::Sink => Some((edge_index, -1)),
993 Flow::Source => Some((edge_index, 1)),
994 },
995 _ => None,
996 })
997 .sorted_by(|a, b| a.0.cmp(&b.0))
998 .collect()
999 };
1000
1001 let vertex_set = graph
1002 .iter_nodes_of(&cut.left)
1003 .map(|(node_id, _, _)| VertexSet::from_usize(node_id.into()))
1004 .reduce(|acc, v| acc.join(&v))
1005 .unwrap();
1006
1007 Self {
1008 energies: edges,
1009 external_shift,
1010 vertex_set,
1011 }
1013 }
1014
1015 pub(crate) fn lmb_atom(&self, graph: &Graph, lmb_reps: &[Replacement]) -> Atom {
1016 self.energies
1017 .iter()
1018 .map(|index| {
1019 let mass_symbol = graph.underlying[*index].mass_atom();
1020 let emr_symbols = (0..3)
1021 .map(|i| function!(GS.emr_mom, usize::from(*index), i + 1))
1022 .collect_vec();
1023
1024 (&emr_symbols[0] * &emr_symbols[0]
1025 + &emr_symbols[1] * &emr_symbols[1]
1026 + &emr_symbols[2] * &emr_symbols[2]
1027 + &mass_symbol * &mass_symbol)
1028 .sqrt()
1029 })
1030 .chain(self.external_shift.iter().map(|(index, sign)| {
1031 function!(GS.emr_mom, usize::from(*index), 0) * Atom::num(*sign)
1032 }))
1033 .reduce(|sum, atom| sum + atom)
1034 .unwrap_or_else(Atom::new)
1035 .replace_multiple(lmb_reps)
1036 .replace(parse!("ZERO"))
1037 .with(Atom::new())
1038 .expand() }
1040
1041 pub(crate) fn lmb_atom_simplified(&self, graph: &Graph, lmb_reps: &[Replacement]) -> Atom {
1043 self.energies
1044 .iter()
1045 .map(|index| {
1046 let mass_symbol = graph.underlying[*index].mass_atom();
1047 let emr_symbol = function!(GS.emr_mom, usize::from(*index));
1048
1049 (&emr_symbol * &emr_symbol + &mass_symbol * &mass_symbol).sqrt()
1050 })
1051 .chain(self.external_shift.iter().map(|(index, sign)| {
1052 function!(GS.emr_mom, usize::from(*index), 0) * Atom::num(*sign)
1053 }))
1054 .reduce(|sum, atom| sum + atom)
1055 .unwrap_or_else(Atom::new)
1056 .replace_multiple(lmb_reps)
1057 .replace(parse!("ZERO"))
1058 .with(Atom::new())
1059 .expand() }
1061}
1062
1063define_index! {pub struct GroupEsurfaceId;}
1064
1065pub type EsurfaceCollection = TiVec<EsurfaceID, Esurface>;
1066
1067pub type EsurfaceCache<T> = TiVec<EsurfaceID, T>;
1068
1069#[derive(
1071 Debug,
1072 Copy,
1073 Clone,
1074 Serialize,
1075 Deserialize,
1076 PartialEq,
1077 From,
1078 Into,
1079 Eq,
1080 Encode,
1081 Decode,
1082 Hash,
1083 PartialOrd,
1084 Ord,
1085)]
1086pub struct EsurfaceID(pub usize);
1087
1088pub type ExistingEsurfaces = TiVec<ExistingEsurfaceId, GroupEsurfaceId>;
1090pub type ExistingThresholds = TiVec<ExistingEsurfaceId, EsurfaceID>;
1091
1092pub(crate) fn get_representative<T: Copy>(
1093 esurface_map: &TiVec<GraphGroupPosition, Option<T>>,
1094) -> Result<(GraphGroupPosition, T)> {
1095 for (group_pos, esurface_option) in esurface_map.iter_enumerated() {
1096 if let Some(esurface_id) = esurface_option {
1097 return Ok((group_pos, *esurface_id));
1098 }
1099 }
1100
1101 Err(eyre!(
1102 "No representative esurface found, esurface map corrupted"
1103 ))
1104}
1105
1106#[derive(
1108 Debug,
1109 From,
1110 Into,
1111 Copy,
1112 Clone,
1113 Serialize,
1114 Deserialize,
1115 PartialEq,
1116 Eq,
1117 Hash,
1118 PartialOrd,
1119 Ord,
1120 Encode,
1121 Decode,
1122)]
1123pub struct ExistingEsurfaceId(usize);
1124
1125impl Display for ExistingEsurfaceId {
1126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1127 write!(f, "ExistingEsurfaceID({})", self.0)
1128 }
1129}
1130
1131pub type ExternalShift = Vec<(EdgeIndex, i64)>;
1132
1133pub(crate) fn add_external_shifts(lhs: &ExternalShift, rhs: &ExternalShift) -> ExternalShift {
1135 let mut res = lhs.clone();
1136
1137 for rhs_element in rhs.iter() {
1138 if let Some(lhs_element) = res
1139 .iter_mut()
1140 .find(|lhs_element| rhs_element.0 == lhs_element.0)
1141 {
1142 lhs_element.1 += rhs_element.1;
1143 } else {
1144 res.push(*rhs_element)
1145 }
1146 }
1147
1148 res.retain(|(_index, sign)| *sign != 0);
1149 res.sort_by_key(|(index, _)| *index);
1150 res
1151}
1152
1153impl From<EsurfaceID> for Atom {
1154 fn from(id: EsurfaceID) -> Self {
1155 parse!(&format!("η({})", Into::<usize>::into(id.0)))
1156 }
1157}
1158
1159define_index!(
1160 pub struct RaisedEsurfaceId;
1161);
1162
1163#[derive(Debug, Clone, Encode, Decode)]
1164#[trait_decode(trait = GammaLoopContext)]
1165pub struct RaisedEsurfaceData {
1166 pub raised_groups: TiVec<RaisedEsurfaceId, RaisedEsurfaceGroup>,
1167 pub pass_two_evaluator: Option<Vec<GenericEvaluator>>,
1168}
1169
1170#[derive(Debug, Clone, Encode, Decode, PartialEq, Hash, Eq, PartialOrd, Ord)]
1171pub struct RaisedEsurfaceGroup {
1172 pub esurface_ids: Vec<EsurfaceID>,
1173 pub max_occurence: usize,
1174}
1175
1176impl Graph {
1177 pub(crate) fn determine_raised_esurfaces_from_expression(
1178 &self,
1179 expr: &CFFExpression<OrientationID>,
1180 ) -> RaisedEsurfaceData {
1181 let raised_edges = self.get_raised_edge_groups();
1182
1183 let normalized_cut_esurfaces = self
1184 .surface_cache
1185 .esurface_cache
1186 .iter()
1187 .map(|esurface| {
1188 let mut new_esurface = esurface.clone();
1189 for energy in new_esurface.energies.iter_mut() {
1190 let group_index_of_energy =
1191 raised_edges.iter().position(|group| group.contains(energy));
1192
1193 if let Some(found_group_index) = group_index_of_energy {
1194 *energy = *raised_edges[found_group_index].first().unwrap();
1195 }
1196 }
1197 new_esurface.energies.sort();
1198 new_esurface
1199 })
1200 .collect::<TiVec<EsurfaceID, _>>();
1201
1202 let mut raised_groups = TiVec::<RaisedEsurfaceId, RaisedEsurfaceGroup>::new();
1203
1204 for (esurface_id, normalized_cut_esurface) in normalized_cut_esurfaces.iter_enumerated() {
1205 let raised_esurface_group_id = raised_groups.iter_enumerated().find_map(
1206 |(raised_esurface_group_id, esurface_group)| {
1207 if esurface_group
1208 .esurface_ids
1209 .iter()
1210 .all(|esurface_id_in_group| {
1211 normalized_cut_esurfaces[*esurface_id_in_group].energies
1212 == normalized_cut_esurface.energies
1213 && normalized_cut_esurfaces[*esurface_id_in_group].external_shift
1214 == normalized_cut_esurface.external_shift
1215 })
1216 {
1217 Some(raised_esurface_group_id)
1218 } else {
1219 None
1220 }
1221 },
1222 );
1223
1224 if let Some(found_group_id) = raised_esurface_group_id {
1225 raised_groups[found_group_id].esurface_ids.push(esurface_id);
1226 } else {
1227 raised_groups.push(RaisedEsurfaceGroup {
1228 esurface_ids: vec![esurface_id],
1229 max_occurence: 0,
1230 });
1231 }
1232 }
1233
1234 let mut result = RaisedEsurfaceData {
1235 raised_groups,
1236 pass_two_evaluator: None,
1237 };
1238
1239 let mut expression_copy = expr.clone();
1240 expression_copy.normalize_wrt_all_raisings(&result);
1241
1242 for cut_group in result.raised_groups.iter_mut() {
1243 let representative_esurface_id = cut_group.esurface_ids[0];
1244
1245 let max_occurence_for_this_id = expression_copy
1246 .orientations
1247 .iter()
1248 .map(|orientation_expression| {
1249 orientation_expression.expression.max_value_count_on_branch(
1250 &crate::cff::surface::HybridSurfaceID::Esurface(representative_esurface_id),
1251 )
1252 })
1253 .max()
1254 .unwrap_or(0);
1255
1256 cut_group.max_occurence = max_occurence_for_this_id;
1257 }
1258
1259 result
1260 }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use itertools::Itertools;
1266 use linnet::half_edge::HedgeGraph;
1267 use linnet::half_edge::builder::HedgeGraphBuilder;
1268 use linnet::half_edge::involution::{EdgeIndex, Flow, Orientation};
1269 use linnet::half_edge::nodestore::NodeStorageVec;
1270 use linnet::half_edge::subgraph::{SuBitGraph, SubSetLike};
1271 use symbolica::atom::{Atom, AtomCore};
1272 use symbolica::parse;
1273
1274 use crate::cff::cff_graph::VertexSet;
1275 use crate::graph::LoopMomentumBasis;
1276 use crate::momentum::{
1277 FourMomentum, SignOrZero,
1278 sample::{ExternalFourMomenta, ExternalIndex},
1279 signature::LoopExtSignature,
1280 };
1281 use crate::processes::CrossSectionCut;
1282 use crate::{
1283 cff::{esurface::Esurface, generation::ShiftRewrite},
1284 utils::{
1285 DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, ESURFACE_SHIFT_THRESHOLD, F,
1286 external_energy_atom_from_index, test_utils::dummy_hedge_graph,
1287 },
1288 };
1289
1290 use super::{EsurfaceExistence, add_external_shifts};
1291
1292 #[test]
1293 fn classification_preserves_the_previous_existing_predicate() {
1294 let e_cm = F(10.0);
1295 let shift_tolerance = F(ESURFACE_SHIFT_THRESHOLD) * e_cm;
1296 let normalized_margin_tolerance = F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD);
1297 let invariant_tolerance = normalized_margin_tolerance * e_cm * e_cm;
1298
1299 for shift_factor in [-2, -1, 0, 1, 2] {
1300 let shift_part = shift_tolerance * shift_tolerance.from_i64(shift_factor);
1301 for margin_factor in [-2, -1, 0, 1, 2] {
1302 let invariant_margin =
1303 invariant_tolerance * invariant_tolerance.from_i64(margin_factor);
1304 let was_existing =
1305 shift_part < -&shift_tolerance && invariant_margin > invariant_tolerance;
1306 let classification = Esurface::classify_invariant_margin(
1307 &shift_part,
1308 invariant_margin,
1309 &e_cm,
1310 &normalized_margin_tolerance,
1311 );
1312
1313 assert_eq!(
1314 classification.is_existing(),
1315 was_existing,
1316 "existence changed for shift factor {shift_factor} and margin factor {margin_factor}",
1317 );
1318 }
1319 }
1320 }
1321
1322 #[test]
1323 fn invalid_programmatic_tolerances_cannot_invert_classification() {
1324 let e_cm = F(10.0);
1325 let shift_part = F(-1.0);
1326 let invariant_margin = F(0.0);
1327
1328 for tolerance in [
1329 F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1330 F(-DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1331 F(f64::NAN),
1332 F(f64::INFINITY),
1333 ] {
1334 assert!(matches!(
1335 Esurface::classify_invariant_margin(
1336 &shift_part,
1337 invariant_margin,
1338 &e_cm,
1339 &tolerance,
1340 ),
1341 EsurfaceExistence::Pinched { .. }
1342 ));
1343 }
1344 }
1345
1346 #[test]
1347 fn positive_external_energy_filter_uses_energy_conservation() {
1348 let incoming_edges = (0..4).map(EdgeIndex::from).collect_vec();
1349 let outgoing_edges = (4..9).map(EdgeIndex::from).collect_vec();
1350 let shift_is_negative = |external_shift: &[(usize, i64)]| {
1351 Esurface {
1352 energies: vec![],
1353 external_shift: external_shift
1354 .iter()
1355 .map(|(edge, coefficient)| (EdgeIndex::from(*edge), *coefficient))
1356 .collect(),
1357 vertex_set: VertexSet::dummy(),
1358 }
1359 .external_shift_is_strictly_negative_for_positive_energies(
1360 &incoming_edges,
1361 &outgoing_edges,
1362 )
1363 };
1364
1365 assert!(
1366 shift_is_negative(&[(0, -1), (1, -1)]),
1367 "a negative proper subset of incoming energies must be retained"
1368 );
1369 assert!(
1370 shift_is_negative(&[(4, -1)]),
1371 "a negative proper subset of outgoing energies must be retained"
1372 );
1373 assert!(
1374 shift_is_negative(&[(0, -1), (1, -1), (2, -1), (3, -1), (4, 1)]),
1375 "energy conservation turns minus all incoming plus one outgoing into minus the remaining outgoing energies"
1376 );
1377 assert!(
1378 !shift_is_negative(&[
1379 (0, -1),
1380 (1, -1),
1381 (2, -1),
1382 (3, -1),
1383 (4, 1),
1384 (5, 1),
1385 (6, 1),
1386 (7, 1),
1387 (8, 1),
1388 ]),
1389 "the energy-conservation identity is zero rather than strictly negative"
1390 );
1391 assert!(
1392 !shift_is_negative(&[(0, -1), (4, 1)]),
1393 "a sign-indefinite difference must not be accepted from positivity alone"
1394 );
1395 assert!(
1396 !shift_is_negative(&[(9, -1)]),
1397 "a shift outside the external-energy partition must be rejected conservatively"
1398 );
1399 }
1400
1401 #[test]
1402 fn massless_two_to_two_surface_is_existing_away_from_collinear_pinch() {
1403 let dummy_graph = dummy_hedge_graph(4);
1404 let lmb = LoopMomentumBasis {
1405 tree: SuBitGraph::empty(0),
1406 loop_edges: vec![EdgeIndex::from(2)].into(),
1407 ext_edges: vec![].into(),
1408 edge_signatures: dummy_graph
1409 .new_edgevec_from_iter(vec![
1410 LoopExtSignature::from((vec![0], vec![1, 0])),
1411 LoopExtSignature::from((vec![0], vec![0, 1])),
1412 LoopExtSignature::from((vec![1], vec![0, 0])),
1413 LoopExtSignature::from((vec![-1], vec![-1, -1])),
1414 ])
1415 .unwrap(),
1416 };
1417 let esurface = Esurface {
1418 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
1419 external_shift: vec![(EdgeIndex::from(0), -1), (EdgeIndex::from(1), -1)],
1420 vertex_set: VertexSet::dummy(),
1421 };
1422 let masses = dummy_graph.new_edgevec_from_iter(vec![F(0.0); 4]).unwrap();
1423 let classify_pair = |second_spatial_momentum: (f64, f64), threshold: f64| {
1426 let external_momenta = ExternalFourMomenta::from_iter([
1427 FourMomentum::from_args(F(5.0), F(5.0), F(0.0), F(0.0)),
1428 FourMomentum::from_args(
1429 F(5.0),
1430 F(second_spatial_momentum.0),
1431 F(second_spatial_momentum.1),
1432 F(0.0),
1433 ),
1434 ]);
1435 esurface.classify_existence(&external_momenta, &lmb, &masses, &F(10.0), &F(threshold))
1436 };
1437
1438 assert!(matches!(
1439 classify_pair((0.0, 5.0), DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1440 EsurfaceExistence::Existing { .. }
1441 ));
1442 assert!(matches!(
1443 classify_pair((5.0, 0.0), DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1444 EsurfaceExistence::Pinched { .. }
1445 ));
1446 assert!(matches!(
1447 classify_pair((0.0, 5.0), 1.0),
1448 EsurfaceExistence::Pinched { .. }
1449 ));
1450 }
1451
1452 #[test]
1453 fn classifies_existing_pinched_and_non_existing_surfaces() {
1454 let dummy_graph = dummy_hedge_graph(5);
1455 let lmb = LoopMomentumBasis {
1456 tree: SuBitGraph::empty(0),
1457 loop_edges: vec![EdgeIndex::from(2), EdgeIndex::from(3)].into(),
1458 ext_edges: vec![].into(),
1459 edge_signatures: dummy_graph
1460 .new_edgevec_from_iter(vec![
1461 LoopExtSignature::from((vec![0, 0], vec![1])),
1462 LoopExtSignature::from((vec![0, 0], vec![-1])),
1463 LoopExtSignature::from((vec![1, 0], vec![0])),
1464 LoopExtSignature::from((vec![0, 1], vec![0])),
1465 LoopExtSignature::from((vec![1, 1], vec![-1])),
1466 ])
1467 .unwrap(),
1468 };
1469 let esurface = Esurface {
1470 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3), EdgeIndex::from(4)],
1471 external_shift: vec![(EdgeIndex::from(0), -1)],
1472 vertex_set: VertexSet::dummy(),
1473 };
1474 let masses = dummy_graph.new_edgevec_from_iter(vec![F(0.0); 5]).unwrap();
1475
1476 let classification = |energy| {
1477 let external_momenta = ExternalFourMomenta::from_iter([FourMomentum::from_args(
1478 F(energy),
1479 F(-10.0),
1480 F(0.0),
1481 F(0.0),
1482 )]);
1483 esurface.classify_existence(
1484 &external_momenta,
1485 &lmb,
1486 &masses,
1487 &F(10.0),
1488 &F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
1489 )
1490 };
1491
1492 assert!(matches!(
1493 classification(11.0),
1494 EsurfaceExistence::Existing { .. }
1495 ));
1496 assert!(matches!(
1497 classification(10.0),
1498 EsurfaceExistence::Pinched { .. }
1499 ));
1500 assert!(matches!(
1501 classification(10.0 + 1.0e-8),
1502 EsurfaceExistence::Pinched { .. }
1503 ));
1504 assert!(matches!(
1505 classification(9.0),
1506 EsurfaceExistence::NonExisting { .. }
1507 ));
1508 }
1509
1510 #[test]
1511 fn test_esurface() {
1512 let dummy_graph = dummy_hedge_graph(5);
1513
1514 let _energies_cache = dummy_graph
1515 .new_edgevec_from_iter([F(1.), F(2.), F(3.), F(4.), F(5.)])
1516 .unwrap();
1517
1518 let energies = vec![EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(2)];
1519
1520 let external_shift = vec![(EdgeIndex::from(3), 1), (EdgeIndex::from(4), 1)];
1521
1522 let mut esurface = Esurface {
1523 energies,
1524 external_shift,
1525 vertex_set: VertexSet::dummy(),
1526 };
1528
1529 let shift_rewrite = ShiftRewrite {
1530 dependent_momentum: EdgeIndex::from(4),
1531 dependent_momentum_expr: vec![
1532 (EdgeIndex::from(1), -1),
1533 (EdgeIndex::from(2), -1),
1534 (EdgeIndex::from(3), -1),
1535 ],
1536 };
1537
1538 esurface.canonicalize_shift(&shift_rewrite);
1539
1540 assert_eq!(
1541 esurface.external_shift,
1542 vec![(EdgeIndex::from(1), -1), (EdgeIndex::from(2), -1)]
1543 );
1544
1545 let energies = vec![EdgeIndex::from(0), EdgeIndex::from(2)];
1546
1547 let external_shift = vec![(EdgeIndex::from(1), -1)];
1548
1549 let _esurface = Esurface {
1550 energies,
1551 external_shift,
1552 vertex_set: VertexSet::dummy(),
1553 };
1555 }
1556
1557 #[test]
1558 fn to_atom_in_lmb_uses_canonical_external_edges_not_carrier_edges() {
1559 let dummy_graph = dummy_hedge_graph(9);
1560 let mut edge_signatures = dummy_graph
1561 .new_edgevec_from_iter(
1562 (0..9).map(|_| LoopExtSignature::from((Vec::<isize>::new(), vec![0, 0]))),
1563 )
1564 .unwrap();
1565 edge_signatures[EdgeIndex::from(8)] =
1566 LoopExtSignature::from((Vec::<isize>::new(), vec![0, 1]));
1567 let lmb = LoopMomentumBasis {
1568 tree: SuBitGraph::empty(0),
1569 loop_edges: vec![].into(),
1570 ext_edges: vec![EdgeIndex::from(2), EdgeIndex::from(6)].into(),
1571 edge_signatures,
1572 };
1573 let esurface = Esurface {
1574 energies: vec![],
1575 external_shift: vec![(EdgeIndex::from(8), -1)],
1576 vertex_set: VertexSet::dummy(),
1577 };
1578
1579 let atom = esurface.to_atom_in_lmb(&[], &lmb).expand();
1580 let expected =
1581 (Atom::num(-1) * external_energy_atom_from_index(EdgeIndex::from(6))).expand();
1582
1583 assert_eq!(atom.to_canonical_string(), expected.to_canonical_string());
1584 }
1585
1586 #[test]
1587 fn shift_part_uses_expanded_global_external_slots() {
1588 let dummy_graph = dummy_hedge_graph(7);
1589 let mut edge_signatures = dummy_graph
1590 .new_edgevec_from_iter(
1591 (0..7).map(|_| LoopExtSignature::from((Vec::<isize>::new(), vec![0]))),
1592 )
1593 .unwrap();
1594 edge_signatures[EdgeIndex::from(6)] =
1595 LoopExtSignature::from((Vec::<isize>::new(), vec![1]));
1596 let mut lmb = LoopMomentumBasis {
1597 tree: SuBitGraph::empty(0),
1598 loop_edges: vec![].into(),
1599 ext_edges: vec![EdgeIndex::from(6)].into(),
1600 edge_signatures,
1601 };
1602 lmb.canonicalize_external_order(&(0..7).map(EdgeIndex::from).collect::<Vec<EdgeIndex>>());
1603
1604 assert_eq!(lmb.ext_edges.len(), 7);
1605 assert_eq!(
1606 lmb.edge_signatures[EdgeIndex::from(6)].external[ExternalIndex(2)],
1607 SignOrZero::Zero
1608 );
1609 assert_eq!(
1610 lmb.edge_signatures[EdgeIndex::from(6)].external[ExternalIndex(6)],
1611 SignOrZero::Plus
1612 );
1613
1614 let external_moms: ExternalFourMomenta<F<f64>> = vec![
1615 FourMomentum::from([F(10.0), F(0.0), F(0.0), F(0.0)]),
1616 FourMomentum::from([F(20.0), F(0.0), F(0.0), F(0.0)]),
1617 FourMomentum::from([F(2000.0), F(0.0), F(0.0), F(0.0)]),
1618 FourMomentum::from([F(30.0), F(0.0), F(0.0), F(0.0)]),
1619 FourMomentum::from([F(40.0), F(0.0), F(0.0), F(0.0)]),
1620 FourMomentum::from([F(50.0), F(0.0), F(0.0), F(0.0)]),
1621 FourMomentum::from([F(438.555), F(0.0), F(0.0), F(0.0)]),
1622 ]
1623 .into();
1624 let esurface = Esurface {
1625 energies: vec![],
1626 external_shift: vec![(EdgeIndex::from(6), -1)],
1627 vertex_set: VertexSet::dummy(),
1628 };
1629
1630 assert_eq!(
1631 esurface.compute_shift_part_from_momenta(&external_moms, &lmb),
1632 F(-438.555)
1633 );
1634 }
1635
1636 #[test]
1637 fn test_add_external_shifts() {
1638 let shift_1 = vec![
1639 (EdgeIndex::from(0), 1),
1640 (EdgeIndex::from(1), 1),
1641 (EdgeIndex::from(2), -1),
1642 ];
1643 let shift_2 = vec![(EdgeIndex::from(1), -1), (EdgeIndex::from(2), 1)];
1644
1645 let add = add_external_shifts(&shift_1, &shift_2);
1646
1647 assert_eq!(add, vec![(EdgeIndex::from(0), 1)]);
1648
1649 let shift_3 = vec![(EdgeIndex::from(3), 1), (EdgeIndex::from(4), -1)];
1650 let shift_4 = vec![
1651 (EdgeIndex::from(0), 1),
1652 (EdgeIndex::from(1), 1),
1653 (EdgeIndex::from(2), 1),
1654 (EdgeIndex::from(4), 1),
1655 ];
1656
1657 let add = add_external_shifts(&shift_3, &shift_4);
1658
1659 assert_eq!(
1660 add,
1661 vec![
1662 (EdgeIndex::from(0), 1),
1663 (EdgeIndex::from(1), 1),
1664 (EdgeIndex::from(2), 1),
1665 (EdgeIndex::from(3), 1)
1666 ]
1667 );
1668 }
1669
1670 #[test]
1671 fn test_esurface_equality() {
1672 let esurface_1 = Esurface {
1673 energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
1674 external_shift: vec![(EdgeIndex::from(0), 1), (EdgeIndex::from(1), 1)],
1675 vertex_set: VertexSet::dummy(),
1676 };
1678
1679 let esurface_2 = Esurface {
1680 energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
1681 external_shift: vec![(EdgeIndex::from(0), 1), (EdgeIndex::from(1), 1)],
1682 vertex_set: VertexSet::dummy(),
1683 };
1685
1686 assert_eq!(esurface_1, esurface_2);
1687 }
1688
1689 mod failing {
1690 use super::*;
1691
1692 #[test]
1693 fn test_to_atom() {
1694 let external_shift = vec![(EdgeIndex::from(1), -1)];
1695
1696 let esurface = Esurface {
1697 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
1698 external_shift,
1699 vertex_set: VertexSet::dummy(),
1700 };
1702
1703 let esurface_atom = esurface.to_atom(&[]);
1704 let expected_atom = parse!("Q(2, cind(0)) + Q(3, cind(0)) - P(1, cind(0))");
1705
1706 let diff = esurface_atom - &expected_atom;
1707 let diff = diff.expand();
1708 assert_eq!(diff, Atom::new());
1709 }
1710
1711 #[test]
1712 fn test_from_cut_left_dt() {
1713 let mut hedge_graph_builder = HedgeGraphBuilder::new();
1714 let nodes = (0..4)
1715 .map(|_| hedge_graph_builder.add_node(()))
1716 .collect_vec();
1717
1718 hedge_graph_builder.add_edge(nodes[0], nodes[1], (), Orientation::Undirected);
1719 hedge_graph_builder.add_edge(nodes[0], nodes[2], (), Orientation::Undirected);
1720 hedge_graph_builder.add_edge(nodes[1], nodes[2], (), Orientation::Undirected);
1721 hedge_graph_builder.add_edge(nodes[1], nodes[3], (), Orientation::Undirected);
1722 hedge_graph_builder.add_edge(nodes[2], nodes[3], (), Orientation::Undirected);
1723
1724 hedge_graph_builder.add_external_edge(
1725 nodes[0],
1726 (),
1727 Orientation::Undirected,
1728 Flow::Sink,
1729 );
1730 hedge_graph_builder.add_external_edge(
1731 nodes[3],
1732 (),
1733 Orientation::Undirected,
1734 Flow::Source,
1735 );
1736
1737 let double_triangle: HedgeGraph<(), (), ()> =
1738 hedge_graph_builder.build::<NodeStorageVec<()>>();
1739 let node_0 = double_triangle.iter_crown(nodes[0]).into();
1740 let node_3 = double_triangle.iter_crown(nodes[3]).into();
1741
1742 let cuts = double_triangle.all_cuts(node_0, node_3);
1743
1744 let cross_section_cuts = cuts
1745 .into_iter()
1746 .map(|(node_l, cut, node_r)| CrossSectionCut {
1747 cut,
1748 left: node_l,
1749 right: node_r,
1750 })
1751 .map(|cut| Esurface::new_from_cut_left(&double_triangle, &cut, None))
1752 .collect_vec();
1753
1754 let expected_esurfaces = vec![
1755 Esurface {
1756 energies: vec![EdgeIndex::from(0), EdgeIndex::from(1)],
1757 external_shift: vec![(EdgeIndex::from(5), -1)],
1758 vertex_set: VertexSet::dummy(),
1759 },
1761 Esurface {
1762 energies: vec![EdgeIndex::from(0), EdgeIndex::from(2), EdgeIndex::from(4)],
1763 external_shift: vec![(EdgeIndex::from(5), -1)],
1764 vertex_set: VertexSet::dummy(),
1765 },
1767 Esurface {
1768 energies: vec![EdgeIndex::from(3), EdgeIndex::from(4)],
1769 external_shift: vec![(EdgeIndex::from(5), -1)],
1770 vertex_set: VertexSet::dummy(),
1771 },
1773 Esurface {
1774 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2), EdgeIndex::from(3)],
1775 external_shift: vec![(EdgeIndex::from(5), -1)],
1776 vertex_set: VertexSet::dummy(),
1777 },
1779 ];
1780
1781 for expected_esurface in expected_esurfaces {
1782 assert!(cross_section_cuts.contains(&expected_esurface));
1783 }
1784 }
1785
1786 #[test]
1787 fn test_from_cut_left_box() {
1788 let mut hedge_graph_builder = HedgeGraphBuilder::new();
1789 let nodes = (0..4)
1790 .map(|_| hedge_graph_builder.add_node(()))
1791 .collect_vec();
1792
1793 hedge_graph_builder.add_edge(nodes[0], nodes[1], (), Orientation::Undirected);
1794 hedge_graph_builder.add_edge(nodes[1], nodes[2], (), Orientation::Undirected);
1795 hedge_graph_builder.add_edge(nodes[2], nodes[3], (), Orientation::Undirected);
1796 hedge_graph_builder.add_edge(nodes[3], nodes[0], (), Orientation::Undirected);
1797
1798 hedge_graph_builder.add_external_edge(
1799 nodes[0],
1800 (),
1801 Orientation::Undirected,
1802 Flow::Sink,
1803 );
1804 hedge_graph_builder.add_external_edge(
1805 nodes[1],
1806 (),
1807 Orientation::Undirected,
1808 Flow::Source,
1809 );
1810 hedge_graph_builder.add_external_edge(
1811 nodes[2],
1812 (),
1813 Orientation::Undirected,
1814 Flow::Source,
1815 );
1816 hedge_graph_builder.add_external_edge(
1817 nodes[3],
1818 (),
1819 Orientation::Undirected,
1820 Flow::Sink,
1821 );
1822
1823 let box_graph: HedgeGraph<(), (), ()> =
1824 hedge_graph_builder.build::<NodeStorageVec<()>>();
1825
1826 let node_0 = box_graph.iter_crown(nodes[0]).into();
1827 let node_2 = box_graph.iter_crown(nodes[2]).into();
1828
1829 let cuts = box_graph.all_cuts(node_0, node_2);
1830 assert_eq!(cuts.len(), 4);
1831
1832 let cross_section_cuts = cuts
1833 .into_iter()
1834 .map(|(node_l, cut, node_r)| CrossSectionCut {
1835 cut,
1836 left: node_l,
1837 right: node_r,
1838 })
1839 .map(|cut| Esurface::new_from_cut_left(&box_graph, &cut, None))
1840 .collect_vec();
1841
1842 let expected_esurfaces = vec![
1843 Esurface {
1844 energies: vec![EdgeIndex::from(0), EdgeIndex::from(3)],
1845 external_shift: vec![(EdgeIndex::from(4), -1)],
1846 vertex_set: VertexSet::dummy(),
1847 },
1849 Esurface {
1850 energies: vec![EdgeIndex::from(0), EdgeIndex::from(2)],
1851 external_shift: vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(7), -1)],
1852 vertex_set: VertexSet::dummy(),
1853 },
1855 Esurface {
1856 energies: vec![EdgeIndex::from(1), EdgeIndex::from(3)],
1857 external_shift: vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(5), 1)],
1858 vertex_set: VertexSet::dummy(),
1859 },
1861 Esurface {
1862 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
1863 external_shift: vec![
1864 (EdgeIndex::from(4), -1),
1865 (EdgeIndex::from(5), 1),
1866 (EdgeIndex::from(7), -1),
1867 ],
1868 vertex_set: VertexSet::dummy(),
1869 },
1871 ];
1872
1873 for expected_esurface in expected_esurfaces {
1874 assert!(cross_section_cuts.contains(&expected_esurface));
1875 }
1876 }
1877 }
1878}