Skip to main content

gammalooprs/cff/
esurface.rs

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/// Core esurface struct
45#[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    //#[bincode(with_serde)]
51    //pub subspace_graph: InternalSubGraph,
52}
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/// Pointwise existence classification exposed without the internal diagnostic margin.
77#[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        // Energy conservation makes external-energy coefficient vectors `c` and
179        // `c + lambda * sigma` equivalent, where sigma is +1 for incoming and -1
180        // for outgoing momenta. The shift is strictly negative for all positive
181        // external energies if one representative is component-wise non-positive
182        // and not identically zero.
183        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    /// Compute the value of the esurface from the momenta, needed to check if an arbitrary point
312    /// is inside the esurface
313    #[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        // /!\ In alphaLoop this boundary needed special care so that `mul_unit` could not
360        // create a discontinuous non-pinched-to-pinched transition for a massless 2->2
361        // E-surface sandwich. Such a surface is `Existing` at non-collinear kinematics, where
362        // its invariant margin is positive, and `Pinched` only at the collinear null boundary.
363        // Here the normalized tolerance band around that boundary is classified as `Pinched`,
364        // so perturbations within the band cannot toggle threshold subtraction. GammaLoop's
365        // statuses are exclusive and only `Existing` surfaces are subtraction targets.
366        // Keep the decision in the original, dimensionful scale. Besides avoiding an
367        // unnecessary division, this preserves the previous existence boundary exactly.
368        // Deserialization rejects invalid configured tolerances. Keep this defensive fallback for
369        // settings mutated programmatically (for example through a language binding), so an
370        // invalid sign can never invert the existence test.
371        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    /// Classify a surface in an active loop-momentum subspace. Pinched and
407    /// non-existing surfaces remain distinct and are never subtraction targets.
408    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        //todo!("refactor for subspaces");
421        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    /// Classify a full-space surface. Pinched and non-existing surfaces remain
515    /// distinct and are never subtraction targets.
516    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        //todo!("refactor for subspaces");
525        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    /// Classify a full-space surface while keeping normalized margins and rejection reasons
564    /// internal to the subtraction implementation.
565    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    /// Only compute the shift part, useful for center finding.
584    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                //panic!("signature: {:?}", signature);
616                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    /// Only compute the shift part, useful for center finding.
628    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 + &center_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    // #[inline]
760    /// the "loops_unit_in_subspace" means that the loop momenta that are part of the subspace are jointly normalized to unit length
761    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        //println!("got to energy loop");
792        for energy in subspace.contains(&self.energies, graph) {
793            debug!("computing contribution for energy {:?}", energy);
794            let signature = &lmb.edge_signatures[energy];
795            //println!("signature {:?}", signature);
796
797            let unit_loop_part =
798                compute_loop_part_subspace(&signature.internal, loops_unit_in_subspace, subspace);
799            //println!("computed_loop_part {:?}", unit_loop_part);
800
801            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            //./bprintln!("computed_shift {:?}", shift);
809
810            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        //println!("got to energy loop");
844        for &energy in &self.energies {
845            //println!("computing contribution for energy {:?}", energy);
846            let signature = &lmb.edge_signatures[energy];
847            //println!("signature {:?}", signature);
848
849            let unit_loop_part = compute_loop_part(&signature.internal, unit_loops);
850            //println!("computed_loop_part {:?}", unit_loop_part);
851
852            let three_shift = compute_shift_part(&signature.external, external_moms).spatial;
853            //./bprintln!("computed_shift {:?}", shift);
854
855            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            //subspace_graph: graph.full_graph(),
1012        }
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() // ensure canonical form
1039    }
1040
1041    // more readable version for debugging, because it doesn't write out components
1042    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() // ensure canonical form
1060    }
1061}
1062
1063define_index! {pub struct GroupEsurfaceId;}
1064
1065pub type EsurfaceCollection = TiVec<EsurfaceID, Esurface>;
1066
1067pub type EsurfaceCache<T> = TiVec<EsurfaceID, T>;
1068
1069/// Index type for esurface, location of an esurface in the list of all esurfaces of a graph
1070#[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
1088/// Container for esurfaces that exist at a given point in the phase space
1089pub 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/// Index in the list of all existing esurfaces, essentially a pointer to a pointer to an esurface
1107#[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
1133/// add two external shifts, eliminates zero signs and sorts
1134pub(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        // Either side of the 2->2 sandwich determines the same total four-momentum by
1424        // momentum conservation, so the incoming pair is sufficient for this classification.
1425        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            //subspace_graph: dummy_graph.full_graph(),
1527        };
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            //subspace_graph: dummy_graph.full_graph(),
1554        };
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            //subspace_graph: unsafe { InternalSubGraph::new_unchecked(SuBitGraph::new()) },
1677        };
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            //subspace_graph: unsafe { InternalSubGraph::new_unchecked(SuBitGraph::new()) },
1684        };
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                // subspace_graph: unsafe { InternalSubGraph::new_unchecked(SuBitGraph::new()) },
1701            };
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                    //subspace_graph: double_triangle.full_graph(),
1760                },
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                    //subspace_graph: double_triangle.full_graph(),
1766                },
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                    //subspace_graph: double_triangle.full_graph(),
1772                },
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                    //subspace_graph: double_triangle.full_graph(),
1778                },
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                    //subspace_graph: box_graph.full_graph(),
1848                },
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                    //subspace_graph: box_graph.full_graph(),
1854                },
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                    //subspace_graph: box_graph.full_graph(),
1860                },
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                    //subspace_graph: box_graph.full_graph(),
1870                },
1871            ];
1872
1873            for expected_esurface in expected_esurfaces {
1874                assert!(cross_section_cuts.contains(&expected_esurface));
1875            }
1876        }
1877    }
1878}