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};
9use linnet::half_edge::subgraph::OrientedCut;
10use ref_ops::RefNeg;
11use rug::{Float, float::Round};
12use serde::{Deserialize, Serialize};
13
14use symbolica::atom::{Atom, AtomCore};
15use symbolica::domains::dual::HyperDual;
16use symbolica::domains::float::{FloatLike as SymFloatLike, Real};
17use symbolica::id::Replacement;
18use symbolica::{function, parse};
19use tracing::debug;
20use typed_index_collections::TiVec;
21
22use crate::cff::VertexSet;
23
24use crate::cff::expression::{
25 CFFExpression, OrientationID, RaisedEsurfaceDataView, RaisedEsurfaceGroupView,
26};
27pub use crate::cff::surface::EsurfaceID;
28use crate::graph::{Graph, GraphGroupPosition, LmbIndex, LoopMomentumBasis};
29use crate::{GammaLoopContext, define_index};
30
31use crate::integrands::process::sampling::maps::SamplingEvaluationError;
32use crate::integrands::process::{GenericEvaluator, ImplicitSurfaceRadialMap};
33use crate::momentum::sample::{
34 ExternalFourMomenta, ExternalIndex, ExternalThreeMomenta, LoopIndex, LoopMomenta, SubspaceData,
35};
36use crate::momentum::{Rotatable, Rotation, ThreeMomentum};
37use crate::processes::CrossSectionCut;
38use crate::utils::hyperdual_utils::new_constant;
39use crate::utils::newton_solver::{
40 NewtonIterationResult, RadialRootDiagnostics, RadialRootIdentity, SafeguardedNewtonError,
41};
42use crate::utils::{
43 ArbPrec, DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, ESURFACE_SHIFT_THRESHOLD, F, FloatLike, GS,
44 Length, compute_loop_part, compute_loop_part_subspace, compute_shift_part,
45 compute_shift_part_subspace, compute_t_part_of_shift_part, cut_energy,
46 external_energy_atom_from_index, ose_atom_from_index,
47};
48use crate::uv::uv_graph::UVE;
49use color_eyre::Result;
50
51use super::generation::ShiftRewrite;
52
53#[derive(Serialize, Deserialize, Debug, Clone, bincode::Encode, bincode::Decode)]
55pub struct Esurface {
56 pub energies: Vec<EdgeIndex>,
57 pub external_shift: ExternalShift,
58 pub vertex_set: VertexSet,
59 }
62
63type EsurfaceRayEnergy<T> = (EdgeIndex, ThreeMomentum<F<T>>, ThreeMomentum<F<T>>, F<T>);
65type EnclosedRayEnergy = ([[Float; 2]; 3], [[Float; 2]; 3], [Float; 2]);
67
68#[derive(Debug, Clone)]
71pub(crate) struct EsurfaceRay<T: FloatLike> {
72 energies: Vec<EsurfaceRayEnergy<T>>,
75 shift: F<T>,
76}
77
78impl EsurfaceRay<ArbPrec> {
79 pub(crate) fn materialize<T: FloatLike>(&self) -> Result<EsurfaceRay<T>> {
82 let spatial = |p: &ThreeMomentum<F<ArbPrec>>| -> Result<ThreeMomentum<F<T>>> {
83 Ok(ThreeMomentum::new(
84 F::from_arb(&p.px.0)?,
85 F::from_arb(&p.py.0)?,
86 F::from_arb(&p.pz.0)?,
87 ))
88 };
89 Ok(EsurfaceRay {
90 energies: self
91 .energies
92 .iter()
93 .map(|(edge, velocity, offset, mass)| {
94 Ok((
95 *edge,
96 spatial(velocity)?,
97 spatial(offset)?,
98 F::from_arb(&mass.0)?,
99 ))
100 })
101 .collect::<Result<_>>()?,
102 shift: F::from_arb(&self.shift.0)?,
103 })
104 }
105}
106
107impl<T: FloatLike> EsurfaceRay<T> {
108 pub(crate) fn to_arb_exact(&self) -> Result<EsurfaceRay<ArbPrec>> {
111 let spatial = |p: &ThreeMomentum<F<T>>| -> Result<ThreeMomentum<F<ArbPrec>>> {
112 Ok(ThreeMomentum::new(
113 p.px.to_arb_exact()?,
114 p.py.to_arb_exact()?,
115 p.pz.to_arb_exact()?,
116 ))
117 };
118 Ok(EsurfaceRay {
119 energies: self
120 .energies
121 .iter()
122 .map(|(edge, velocity, offset, mass)| {
123 Ok((
124 *edge,
125 spatial(velocity)?,
126 spatial(offset)?,
127 mass.to_arb_exact()?,
128 ))
129 })
130 .collect::<Result<_>>()?,
131 shift: self.shift.to_arb_exact()?,
132 })
133 }
134
135 const ENCLOSURE_PRECISION: u32 = 2048;
136
137 fn enclosed_add(a: &[Float; 2], b: &[Float; 2]) -> [Float; 2] {
140 [
141 Float::with_val_round(Self::ENCLOSURE_PRECISION, &a[0] + &b[0], Round::Down).0,
142 Float::with_val_round(Self::ENCLOSURE_PRECISION, &a[1] + &b[1], Round::Up).0,
143 ]
144 }
145 fn enclosed_mul(a: &[Float; 2], b: &[Float; 2]) -> [Float; 2] {
146 let lower = a
147 .iter()
148 .flat_map(|x| {
149 b.iter().map(move |y| {
150 Float::with_val_round(Self::ENCLOSURE_PRECISION, x * y, Round::Down).0
151 })
152 })
153 .reduce(|a, b| a.min(&b))
154 .unwrap();
155 let upper = a
156 .iter()
157 .flat_map(|x| {
158 b.iter().map(move |y| {
159 Float::with_val_round(Self::ENCLOSURE_PRECISION, x * y, Round::Up).0
160 })
161 })
162 .reduce(|a, b| a.max(&b))
163 .unwrap();
164 [lower, upper]
165 }
166 fn enclosed_square(a: &[Float; 2]) -> [Float; 2] {
167 let upper = a[0].clone().abs().max(&a[1].clone().abs());
168 let lower = if a[0] <= 0 && a[1] >= 0 {
169 Float::with_val(Self::ENCLOSURE_PRECISION, 0)
170 } else {
171 a[0].clone().abs().min(&a[1].clone().abs())
172 };
173 [
174 Float::with_val_round(Self::ENCLOSURE_PRECISION, &lower * &lower, Round::Down).0,
175 Float::with_val_round(Self::ENCLOSURE_PRECISION, &upper * &upper, Round::Up).0,
176 ]
177 }
178 fn enclosed_divide(a: &[Float; 2], b: &[Float; 2]) -> [Float; 2] {
179 debug_assert!(b[0] > 0);
180 let reciprocal = [
181 Float::with_val_round(Self::ENCLOSURE_PRECISION, b[1].recip_ref(), Round::Down).0,
182 Float::with_val_round(Self::ENCLOSURE_PRECISION, b[0].recip_ref(), Round::Up).0,
183 ];
184 Self::enclosed_mul(a, &reciprocal)
185 }
186
187 fn enclosed_native(value: &F<T>) -> [Float; 2] {
188 let (lo, hi) = value.0.mpfr_enclosure(Self::ENCLOSURE_PRECISION);
189 [lo, hi]
190 }
191
192 fn enclosed_sum(terms: impl Iterator<Item = (i64, [Float; 2])>) -> [Float; 2] {
193 let zero = Float::with_val(Self::ENCLOSURE_PRECISION, 0);
194 terms.fold([zero.clone(), zero], |sum, (coefficient, value)| {
195 let coefficient = Float::with_val(Self::ENCLOSURE_PRECISION, coefficient);
196 Self::enclosed_add(
197 &sum,
198 &Self::enclosed_mul(&[coefficient.clone(), coefficient], &value),
199 )
200 })
201 }
202
203 fn evaluate_enclosed(
204 energies: impl Iterator<Item = EnclosedRayEnergy>,
205 shift: [Float; 2],
206 radius: &[Float; 2],
207 with_derivative: bool,
208 ) -> Result<([Float; 2], [Float; 2])> {
209 let zero = Float::with_val(Self::ENCLOSURE_PRECISION, 0);
210 let point = |x: &Float| [x.clone(), x.clone()];
211 let uncertain = |detail: &str| SamplingEvaluationError::UncertainGeometry {
212 detail: format!("LU host adoption: {detail}"),
213 };
214 let mut value = shift;
215 let mut slope = point(&zero);
216 for (velocity, offset, mass) in energies {
217 let mut norm = Self::enclosed_square(&mass);
218 let mut dot = point(&zero);
219 for (v, b) in velocity.into_iter().zip(offset) {
220 let q = Self::enclosed_add(&Self::enclosed_mul(&v, radius), &b);
221 norm = Self::enclosed_add(&norm, &Self::enclosed_square(&q));
222 dot = Self::enclosed_add(&dot, &Self::enclosed_mul(&v, &q));
223 }
224 if norm.iter().chain(&dot).any(|x| !x.is_finite()) {
225 return Err(SamplingEvaluationError::Unrepresentable {
226 operation: "LU host certificate",
227 detail: "nonfinite directed energy or numerator".into(),
228 }
229 .into());
230 }
231 let energy = [
232 Float::with_val_round(Self::ENCLOSURE_PRECISION, norm[0].sqrt_ref(), Round::Down).0,
233 Float::with_val_round(Self::ENCLOSURE_PRECISION, norm[1].sqrt_ref(), Round::Up).0,
234 ];
235 value = Self::enclosed_add(&value, &energy);
236 if with_derivative {
237 if energy[0] <= 0 {
238 return Err(uncertain(
239 "energy lower bound touches zero on the fixed candidate interval",
240 )
241 .into());
242 }
243 slope = Self::enclosed_add(&slope, &Self::enclosed_divide(&dot, &energy));
244 }
245 }
246 if value.iter().chain(&slope).any(|x| !x.is_finite()) {
247 return Err(SamplingEvaluationError::Unrepresentable {
248 operation: "LU host certificate",
249 detail: "nonfinite directed residual or derivative".into(),
250 }
251 .into());
252 }
253 Ok((value, slope))
254 }
255
256 pub(crate) fn verify_normal_alignment(
261 canonical_normals: [[Float; 2]; 2],
262 native_normals: [[Float; 2]; 2],
263 native_host_residual: [Float; 2],
264 tolerance: f64,
265 ) -> Result<()> {
266 if !tolerance.is_finite() || tolerance <= 0.0 {
267 return Err(eyre!(
268 "hosted normal alignment requires a finite positive budget"
269 ));
270 }
271 let bounds = canonical_normals
272 .iter()
273 .chain(&native_normals)
274 .chain(std::iter::once(&native_host_residual));
275 for bound in bounds {
276 if bound.iter().any(|x| !x.is_finite()) {
277 return Err(SamplingEvaluationError::Unrepresentable {
278 operation: "hosted normal alignment",
279 detail: "nonfinite directed normal or host residual".into(),
280 }
281 .into());
282 }
283 if bound[0] > bound[1] {
284 return Err(eyre!("hosted normal alignment requires ordered bounds"));
285 }
286 }
287 let radius_squared = Self::enclosed_add(
288 &Self::enclosed_square(&canonical_normals[0]),
289 &Self::enclosed_square(&canonical_normals[1]),
290 );
291 let radius_lower = Float::with_val_round(
292 Self::ENCLOSURE_PRECISION,
293 radius_squared[0].sqrt_ref(),
294 Round::Down,
295 )
296 .0;
297 if radius_lower <= 0 {
298 return Err(SamplingEvaluationError::UncertainGeometry {
299 detail: "hosted normal alignment has no strictly positive canonical radius bound"
300 .into(),
301 }
302 .into());
303 }
304 let difference: [_; 2] = std::array::from_fn(|axis| {
305 let canonical = &canonical_normals[axis];
306 Self::enclosed_add(
307 &native_normals[axis],
308 &[-canonical[1].clone(), -canonical[0].clone()],
309 )
310 });
311 let error_squared = Self::enclosed_add(
312 &Self::enclosed_square(&difference[0]),
313 &Self::enclosed_square(&difference[1]),
314 );
315 let error_upper = Float::with_val_round(
316 Self::ENCLOSURE_PRECISION,
317 error_squared[1].sqrt_ref(),
318 Round::Up,
319 )
320 .0;
321 let budget = Float::with_val(Self::ENCLOSURE_PRECISION, tolerance);
322 let half_budget =
323 Float::with_val_round(Self::ENCLOSURE_PRECISION, &budget / 2, Round::Down).0;
324 let allowed = Float::with_val_round(
325 Self::ENCLOSURE_PRECISION,
326 &half_budget * &radius_lower,
327 Round::Down,
328 )
329 .0;
330 let host_upper = native_host_residual[0]
331 .clone()
332 .abs()
333 .max(&native_host_residual[1].clone().abs());
334 if [&radius_lower, &error_upper, &host_upper, &allowed]
335 .iter()
336 .any(|x| !x.is_finite())
337 {
338 return Err(SamplingEvaluationError::Unrepresentable {
339 operation: "hosted normal alignment",
340 detail: "nonfinite directed comparison".into(),
341 }
342 .into());
343 }
344 if error_upper > allowed || host_upper > allowed {
345 return Err(SamplingEvaluationError::UncertainGeometry {
346 detail: "original normal displacement or completed host defect exceeds its half sampling budget".into(),
347 }.into());
348 }
349 Ok(())
350 }
351
352 #[inline]
353 pub(crate) fn evaluate(&self, radius: &F<T>) -> (F<T>, F<T>) {
354 let zero = radius.zero();
355 let (derivative, energy_sum) = self
356 .energies
357 .iter()
358 .map(|(_, velocity, offset, mass)| {
359 let momentum = velocity * radius + offset;
360 let energy = (momentum.norm_squared() + mass * mass).sqrt();
361 let derivative = if radius == &zero
366 && mass == &zero
367 && momentum.px == zero
368 && momentum.py == zero
369 && momentum.pz == zero
370 {
371 velocity.norm_squared().sqrt()
372 } else {
373 momentum * velocity / &energy
374 };
375 (derivative, energy)
376 })
377 .fold(
378 (zero.clone(), zero.clone()),
379 |(der_sum, en_sum), (der, en)| (der_sum + der, en_sum + en),
380 );
381 (energy_sum + &self.shift, derivative)
382 }
383
384 pub(crate) fn solve_lu_cut(
386 &self,
387 e_cm: &F<T>,
388 diagnostics: &mut RadialRootDiagnostics,
389 identity: &RadialRootIdentity,
390 ) -> std::result::Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
391 let guess = Esurface::radius_guess_from_terms(
392 &self.shift,
393 self.energies
394 .iter()
395 .map(|(_, v, b, _)| (v.norm_squared(), v.clone() * b)),
396 );
397 crate::debug_tags!(#integration, #cut, #solver;
398 radial_root = %identity,
399 initial_guess = %guess,
400 residual_tolerance = %(e_cm * guess.epsilon() * guess.from_i64((4 * self.energies.len() + 1) as i64)),
401 "LU radial root setup"
402 );
403 let residual_tolerance = guess.from_i64((4 * self.energies.len() + 1) as i64);
409 diagnostics.solve(
410 identity,
411 &guess.zero(),
412 &guess,
413 |t| self.evaluate(t),
414 &residual_tolerance,
415 2000,
416 64,
417 e_cm,
418 )
419 }
420
421 pub(crate) fn verify_lu_candidate(
426 &self,
427 completed: &Self,
428 solution: &NewtonIterationResult<T>,
429 dimension_bound: usize,
430 tolerance: &F<T>,
431 ) -> Result<()> {
432 let precision = Self::ENCLOSURE_PRECISION;
433 let uncertain = |detail: &str| SamplingEvaluationError::UncertainGeometry {
434 detail: format!("LU host adoption: {detail}"),
435 };
436 if self
437 .energies
438 .iter()
439 .map(|entry| entry.0)
440 .ne(completed.energies.iter().map(|entry| entry.0))
441 {
442 return Err(eyre!(
443 "LU host adoption has incompatible ordered energy occurrences"
444 ));
445 }
446 for ray in [self, completed] {
447 if !ray.shift.0.is_finite()
448 || ray.energies.iter().any(|(_, v, b, m)| {
449 [&v.px, &v.py, &v.pz, &b.px, &b.py, &b.pz, m]
450 .iter()
451 .any(|x| !x.0.is_finite())
452 })
453 {
454 return Err(SamplingEvaluationError::Unrepresentable {
455 operation: "LU host adoption",
456 detail: "nonfinite represented ray coefficients".into(),
457 }
458 .into());
459 }
460 }
461 let t = &solution.solution;
462 let derivative = &solution.derivative_at_solution;
463 if [t, derivative, tolerance].iter().any(|x| !x.0.is_finite()) {
464 return Err(SamplingEvaluationError::Unrepresentable {
465 operation: "LU host adoption",
466 detail: "nonfinite candidate, derivative or budget".into(),
467 }
468 .into());
469 }
470 if t <= &t.zero() || derivative <= &t.zero() || tolerance <= &t.zero() {
471 return Err(uncertain("candidate, derivative and budget must be positive").into());
472 }
473 let native = Self::enclosed_native;
474 let point = |x: &Float| [x.clone(), x.clone()];
475 let zero = Float::with_val(precision, 0);
476 let one = Float::with_val(precision, 1);
477 let evaluate = |ray: &Self, radius: &[Float; 2], with_derivative: bool| {
478 Self::evaluate_enclosed(
479 ray.energies.iter().map(|(_, velocity, offset, mass)| {
480 (
481 [&velocity.px, &velocity.py, &velocity.pz].map(native),
482 [&offset.px, &offset.py, &offset.pz].map(native),
483 native(mass),
484 )
485 }),
486 native(&ray.shift),
487 radius,
488 with_derivative,
489 )
490 };
491 let delta = native(tolerance)[0]
492 .clone()
493 .min(&(Float::with_val(precision, 1) >> 2));
494 let divisor = Float::with_val(precision, 4 * (dimension_bound + 1));
495 let r = Float::with_val_round(precision, &delta / &divisor, Round::Down).0;
496 let t0 = native(t);
497 let lo_factor = Float::with_val_round(precision, &one - &r, Round::Down).0;
498 let hi_factor = Float::with_val_round(precision, &one + &r, Round::Up).0;
499 let interval = [
500 Float::with_val_round(precision, &t0[0] * &lo_factor, Round::Down).0,
501 Float::with_val_round(precision, &t0[1] * &hi_factor, Round::Up).0,
502 ];
503 if interval
504 .iter()
505 .chain(&t0)
506 .chain([&delta, &r])
507 .any(|x| !x.is_finite())
508 {
509 return Err(SamplingEvaluationError::Unrepresentable {
510 operation: "LU host certificate",
511 detail: "nonfinite directed candidate interval".into(),
512 }
513 .into());
514 }
515 if interval[0] <= 0 || interval[0] >= interval[1] {
516 return Err(uncertain("fixed candidate interval is not positive and distinct").into());
517 }
518 let allowed = [
519 Float::with_val_round(precision, &one - &delta, Round::Up).0,
520 Float::with_val_round(precision, &one + &delta, Round::Down).0,
521 ];
522 let residual_budget = Float::with_val_round(precision, &t0[0] * &r, Round::Down).0;
523 for ray in [self, completed] {
524 if evaluate(ray, &point(&zero), false)?.0[1] >= 0
525 || evaluate(ray, &point(&interval[0]), false)?.0[1] >= 0
526 || evaluate(ray, &point(&interval[1]), false)?.0[0] <= 0
527 {
528 return Err(uncertain("origin/interior/exterior signs are not certified on the fixed candidate interval").into());
529 }
530 let (_, slope) = evaluate(ray, &interval, true)?;
531 if slope[0] <= 0 {
532 return Err(uncertain(
533 "radial derivative is not certified positive throughout the interval",
534 )
535 .into());
536 }
537 let residual = evaluate(ray, &t0, false)?.0;
538 let residual = residual[0].clone().abs().max(&residual[1].clone().abs());
539 if Float::with_val_round(precision, &residual / &slope[0], Round::Up).0
540 > residual_budget
541 {
542 return Err(uncertain(
543 "candidate error exceeds its allocated relative root budget",
544 )
545 .into());
546 }
547 let ratio = Self::enclosed_divide(&slope, &native(derivative));
548 if ratio.iter().any(|x| !x.is_finite())
549 || ratio[0] < allowed[0]
550 || ratio[1] > allowed[1]
551 {
552 return Err(
553 uncertain("first derivative variation exceeds the sampling budget").into(),
554 );
555 }
556 }
557 for ratio in [
560 Self::enclosed_divide(&interval, &t0),
561 Self::enclosed_divide(&t0, &interval),
562 ] {
563 let mut determinant_ratio = point(&one);
564 for _ in 0..dimension_bound {
565 determinant_ratio = Self::enclosed_mul(&determinant_ratio, &ratio);
566 }
567 if determinant_ratio.iter().any(|x| !x.is_finite())
568 || determinant_ratio[0] < allowed[0]
569 || determinant_ratio[1] > allowed[1]
570 {
571 return Err(uncertain(
572 "worst-case LU inverse-volume variation exceeds the sampling budget",
573 )
574 .into());
575 }
576 }
577 Ok(())
578 }
579
580 #[inline]
583 pub(crate) fn evaluate_dual(&self, radius: &HyperDual<F<T>>) -> HyperDual<F<T>> {
584 let energy_sum = self
585 .energies
586 .iter()
587 .map(|(_, velocity, offset, mass)| {
588 let momentum = velocity.map_ref(&|v| new_constant(radius, v) * radius)
589 + offset.map_ref(&|b| new_constant(radius, b));
590 (momentum.norm_squared() + mass * mass).sqrt()
591 })
592 .reduce(|sum, energy| sum + energy)
593 .unwrap_or_else(|| radius.zero());
594 energy_sum + new_constant(radius, &self.shift)
595 }
596}
597
598impl<T: FloatLike> Rotatable for EsurfaceRay<T> {
599 fn rotate(&self, rotation: &Rotation) -> Self {
600 Self {
601 energies: self
602 .energies
603 .iter()
604 .map(|(edge, v, b, mass)| {
605 (*edge, v.rotate(rotation), b.rotate(rotation), mass.clone())
606 })
607 .collect(),
608 shift: self.shift.clone(),
609 }
610 }
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614pub(crate) enum NonExistingEsurfaceReason {
615 NoExternalShift,
616 ShiftNotNegative,
617 NoRadialDependence,
618 NoRealZero,
619}
620
621#[derive(Debug, Clone)]
622pub(crate) enum EsurfaceExistence<T: FloatLike> {
623 NonExisting {
624 normalized_margin: Option<F<T>>,
625 reason: NonExistingEsurfaceReason,
626 },
627 Pinched {
628 normalized_margin: F<T>,
629 },
630 Existing {
631 normalized_margin: F<T>,
632 },
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637pub enum EsurfaceExistenceStatus {
638 NonExisting,
639 Pinched,
640 Existing,
641}
642
643impl<T: FloatLike> EsurfaceExistence<T> {
644 fn status(&self) -> EsurfaceExistenceStatus {
645 match self {
646 Self::NonExisting { .. } => EsurfaceExistenceStatus::NonExisting,
647 Self::Pinched { .. } => EsurfaceExistenceStatus::Pinched,
648 Self::Existing { .. } => EsurfaceExistenceStatus::Existing,
649 }
650 }
651
652 pub(crate) fn is_existing(&self) -> bool {
653 matches!(self, Self::Existing { .. })
654 }
655
656 pub(crate) fn normalized_margin(&self) -> Option<&F<T>> {
657 match self {
658 Self::NonExisting {
659 normalized_margin, ..
660 } => normalized_margin.as_ref(),
661 Self::Pinched { normalized_margin } | Self::Existing { normalized_margin } => {
662 Some(normalized_margin)
663 }
664 }
665 }
666
667 pub(crate) fn label(&self) -> &'static str {
668 match self {
669 Self::NonExisting { .. } => "non_existing",
670 Self::Pinched { .. } => "pinched",
671 Self::Existing { .. } => "existing",
672 }
673 }
674
675 pub(crate) fn non_existing_reason(&self) -> Option<NonExistingEsurfaceReason> {
676 match self {
677 Self::NonExisting { reason, .. } => Some(*reason),
678 Self::Pinched { .. } | Self::Existing { .. } => None,
679 }
680 }
681}
682
683pub(crate) fn esurface_value_is_strictly_inside<T: FloatLike>(value: &F<T>, e_cm: &F<T>) -> bool {
684 let interior_tolerance = value.epsilon() * value.from_i64(8) * e_cm;
685 !value.is_nan() && !value.is_infinite() && value < &(-interior_tolerance)
686}
687
688impl PartialEq for Esurface {
689 fn eq(&self, other: &Self) -> bool {
690 self.energies == other.energies && self.external_shift == other.external_shift
691 }
692}
693
694impl Eq for Esurface {}
695
696impl Esurface {
697 #[allow(clippy::too_many_arguments)]
706 pub(crate) fn sampling_joint_geometry_in_subspace<T: FloatLike>(
707 &self,
708 other: &Self,
709 subspace: &SubspaceData,
710 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
711 graph: &Graph,
712 masses: &EdgeVec<F<T>>,
713 external_momenta: &ExternalFourMomenta<F<T>>,
714 complement: &[LoopIndex],
715 ) -> Result<(
716 crate::integrands::process::sampling::joint::SharedEnergyJointGeometryEvaluator<T>,
717 crate::momentum::signature::LoopExtSignature,
718 )> {
719 use crate::integrands::process::{
720 SharedEnergyJointGeometry, sampling::maps::SamplingEvaluationError,
721 };
722 use crate::momentum::{SignOrZero, signature::LoopExtSignature};
723 use std::sync::Arc;
724
725 let lmb = subspace.get_lmb(all_lmbs);
726 let active = subspace.iter_lmb_indices().collect_vec();
727 let [active] = active.as_slice() else {
728 return Err(eyre!(
729 "joint energy sampling requires exactly one active loop cycle"
730 ));
731 };
732 let active = *active;
733 let mut covered = complement.iter().copied().chain([active]).collect_vec();
734 covered.sort();
735 if covered.iter().any(|index| index.0 >= lmb.loop_edges.len())
736 || covered.windows(2).any(|pair| pair[0] == pair[1])
737 {
738 return Err(eyre!(
739 "joint energy sampling has repeated or invalid active/prior loop indices"
740 ));
741 }
742 if external_momenta.is_empty() || external_momenta.len() != lmb.ext_edges.len() {
743 return Err(eyre!(
744 "joint energy sampling requires all {} external ports, received {}",
745 lmb.ext_edges.len(),
746 external_momenta.len()
747 ));
748 }
749 let surfaces = [self, other];
750 for &surface in &surfaces {
751 for &edge in &surface.energies {
752 let signature = &lmb.edge_signatures[edge];
753 for index in (0..lmb.loop_edges.len())
754 .map(LoopIndex)
755 .filter(|index| !covered.contains(index))
756 {
757 if signature.internal[index] != SignOrZero::Zero {
758 return Err(eyre!(
759 "joint energy surface {:?} depends on unsampled parent edge {}",
760 surface.energies,
761 lmb.loop_edges[index]
762 ));
763 }
764 }
765 if !masses[edge].0.is_finite() {
766 return Err(SamplingEvaluationError::Unrepresentable {
767 operation: "joint energy mass",
768 detail: format!("non-finite mass on edge {edge}"),
769 }
770 .into());
771 }
772 if masses[edge] < masses[edge].zero() {
773 return Err(eyre!(
774 "joint energy edge {edge} requires a nonnegative mass"
775 ));
776 }
777 }
778 }
779 let varying = surfaces.map(|surface| {
780 surface
781 .energies
782 .iter()
783 .copied()
784 .filter(|edge| lmb.edge_signatures[*edge].internal[active] != SignOrZero::Zero)
785 .sorted_by_key(|edge| edge.0)
786 .collect_vec()
787 });
788 if varying.iter().any(|edges| edges.len() != 2) {
789 return Err(eyre!(
790 "joint energy sampling requires exactly two varying energy occurrences per equation, got {:?}",
791 varying
792 ));
793 }
794 for (surface, varying) in surfaces.iter().zip(&varying) {
795 let contained = subspace.contains(&surface.energies, graph).collect_vec();
796 if varying.iter().any(|edge| !contained.contains(edge)) {
797 return Err(eyre!(
798 "joint varying energy routing is inconsistent with its selected cycle subgraph"
799 ));
800 }
801 }
802 let shifts =
805 surfaces.map(|surface| surface.compute_shift_part_from_momenta(external_momenta, lmb));
806 let externals: ExternalThreeMomenta<F<T>> =
807 external_momenta.iter().map(|p| p.spatial.clone()).collect();
808 if shifts
809 .iter()
810 .chain(externals.iter().flat_map(|p| [&p.px, &p.py, &p.pz]))
811 .any(|value| !value.0.is_finite())
812 {
813 return Err(SamplingEvaluationError::Unrepresentable {
814 operation: "joint external data",
815 detail: "non-finite external spatial vector or original energy shift".into(),
816 }
817 .into());
818 }
819 let mut shared = Vec::new();
820 for (left, &a) in varying[0].iter().enumerate() {
821 for (right, &b) in varying[1].iter().enumerate() {
822 if graph[a].mass_atom() == graph[b].mass_atom()
823 && lmb.edge_signatures[a]
824 .spatial_equality_up_to_sign(&lmb.edge_signatures[b], &externals)?
825 {
826 shared.push((left, right));
827 }
828 }
829 }
830 let [(left, right)] = shared.as_slice() else {
831 return Err(eyre!(
832 "joint energy sampling requires exactly one common full signed routing in the supplied spatial frame and mass expression; found {} candidates for {:?}",
833 shared.len(),
834 varying
835 ));
836 };
837 let edges = [
838 varying[0][*left],
839 varying[0][1 - *left],
840 varying[1][1 - *right],
841 ];
842 if masses[edges[0]] != masses[varying[1][*right]] {
843 return Err(eyre!(
844 "joint shared mass expression has inconsistent native values on edges {} and {}",
845 edges[0],
846 varying[1][*right]
847 ));
848 }
849 let canonical = |edge| {
850 let signature = &lmb.edge_signatures[edge];
851 if signature.internal[active] == SignOrZero::Minus {
852 LoopExtSignature {
853 internal: signature.internal.iter().map(|sign| -*sign).collect(),
854 external: signature.external.iter().map(|sign| -*sign).collect(),
855 }
856 } else {
857 signature.clone()
858 }
859 };
860 let routes = edges.map(canonical);
861 let common = routes[0].clone();
862 let fixed = surfaces.map(|surface| {
863 surface
864 .energies
865 .iter()
866 .copied()
867 .filter(|edge| lmb.edge_signatures[*edge].internal[active] == SignOrZero::Zero)
868 .collect_vec()
869 });
870 let lmb = lmb.clone();
871 let masses = masses.clone();
872 let complement = complement.to_vec();
873 let geometry = Arc::new(move |context: &[T]| {
874 if context.len() != 3 * complement.len() {
875 return Err(eyre!(
876 "joint energy context has {} components, expected {}",
877 context.len(),
878 3 * complement.len()
879 ));
880 }
881 if context.iter().any(|x| !x.is_finite()) {
882 return Err(SamplingEvaluationError::Unrepresentable {
883 operation: "joint energy context",
884 detail: "non-finite declared prerequisite".into(),
885 }
886 .into());
887 }
888 let zero = masses[edges[0]].zero();
889 let mut loops = LoopMomenta::from_iter(
890 (0..lmb.loop_edges.len())
891 .map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
892 );
893 for (&index, point) in complement.iter().zip(context.as_chunks::<3>().0.iter()) {
894 loops[index] = ThreeMomentum::new(
895 F(point[0].clone()),
896 F(point[1].clone()),
897 F(point[2].clone()),
898 );
899 }
900 let offsets: [ThreeMomentum<F<T>>; 3] = routes
901 .each_ref()
902 .map(|route| route.compute_momentum(&loops, &externals));
903 let differences = [&offsets[1] - &offsets[0], &offsets[2] - &offsets[0]];
904 let sums = fixed.iter().enumerate().map(|(index, fixed)| {
905 let fixed_sum = fixed.iter().try_fold(zero.clone(), |sum, &edge| -> Result<F<T>> {
906 let momentum: ThreeMomentum<F<T>> = lmb.edge_signatures[edge].compute_momentum(&loops, &externals);
907 let energy = (momentum.norm_squared()+masses[edge].square()).sqrt();
908 if energy == zero && (masses[edge] != zero || momentum.px != zero || momentum.py != zero || momentum.pz != zero) {
909 return Err(SamplingEvaluationError::Unrepresentable { operation: "joint fixed energy", detail: format!("nonzero mass or momentum on edge {edge} produced zero energy") }.into());
910 }
911 Ok(sum+energy)
912 })?;
913 Ok((-(&shifts[index]+fixed_sum)).0)
914 }).collect::<Result<Vec<T>>>()?;
915 let energy_sums = [sums[0].clone(), sums[1].clone()];
916 let geometry = SharedEnergyJointGeometry {
917 shifts: differences.map(|v| [v.px.0, v.py.0, v.pz.0]),
918 masses: edges.map(|edge| masses[edge].0.clone()),
919 energy_sums,
920 };
921 if geometry
922 .shifts
923 .iter()
924 .flatten()
925 .chain(&geometry.energy_sums)
926 .any(|x| !x.is_finite())
927 {
928 return Err(SamplingEvaluationError::Unrepresentable {
929 operation: "joint prepared energies",
930 detail: "non-finite routed offset or fixed energy sum".into(),
931 }
932 .into());
933 }
934 Ok(geometry)
935 });
936 Ok((geometry, common))
937 }
938
939 pub(crate) fn sampling_radial_map<T: FloatLike>(
944 &self,
945 lmb: &LoopMomentumBasis,
946 masses: &EdgeVec<F<T>>,
947 external_momenta: &ExternalFourMomenta<F<T>>,
948 beta: f64,
949 power: f64,
950 ) -> Result<ImplicitSurfaceRadialMap<T>> {
951 let dimension = 3 * lmb.loop_edges.len();
952 let surface = self.clone();
953 let lmb = lmb.clone();
954 let masses = masses.clone();
955 let external_momenta = external_momenta.clone();
956 let zero = F::<T>::from_f64(0.0);
957 let center = LoopMomenta::from_iter(
958 (0..dimension / 3)
959 .map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
960 );
961 let evaluator = std::sync::Arc::new(move |direction: &[T], radius: T| {
962 let radius = F(radius);
963 let unit_loops =
964 LoopMomenta::from_iter(direction.as_chunks::<3>().0.iter().map(|components| {
965 ThreeMomentum::new(
966 F(components[0].clone()),
967 F(components[1].clone()),
968 F(components[2].clone()),
969 )
970 }));
971 let (value, derivative) = surface.compute_self_and_r_derivative(
972 &radius,
973 &unit_loops,
974 ¢er,
975 &external_momenta,
976 &masses,
977 &lmb,
978 );
979 Ok((value.0, derivative.0))
980 });
981 ImplicitSurfaceRadialMap::new(dimension, vec![zero.0; dimension], beta, power, evaluator)
982 }
983
984 #[allow(clippy::too_many_arguments)]
994 pub(crate) fn sampling_radial_map_in_subspace<T: FloatLike>(
995 &self,
996 subspace: &SubspaceData,
997 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
998 graph: &Graph,
999 masses: &EdgeVec<F<T>>,
1000 external_momenta: &ExternalFourMomenta<F<T>>,
1001 complement: &[LoopIndex],
1002 settings: &crate::settings::RuntimeSettings,
1003 beta: f64,
1004 power: f64,
1005 ) -> Result<ImplicitSurfaceRadialMap<T>> {
1006 use crate::integrands::process::PreparedSurfaceStatus;
1007 use crate::integrands::process::sampling::maps::SamplingEvaluationError;
1008 use crate::momentum::SignOrZero;
1009 use crate::subtraction::overlap_subspace::{OverlapInput, find_center};
1010 use std::sync::Arc;
1011 use three_dimensional_reps::utils::rank_i64;
1012
1013 if !settings.kinematics.e_cm.is_finite() || settings.kinematics.e_cm <= 0.0 {
1014 return Err(eyre!("sampling fiber requires finite positive e_cm"));
1015 }
1016 let lmb = subspace.get_lmb(all_lmbs);
1017 let active = subspace.iter_lmb_indices().collect_vec();
1018 let mut covered = active.iter().chain(complement).copied().collect_vec();
1019 covered.sort();
1020 if active.is_empty()
1021 || covered.windows(2).any(|pair| pair[0] == pair[1])
1022 || covered.iter().any(|index| index.0 >= lmb.loop_edges.len())
1023 || external_momenta.len() != lmb.ext_edges.len()
1024 || external_momenta.is_empty()
1025 {
1026 return Err(eyre!(
1027 "sampling fiber requires disjoint active/complement slots in its parent frame and complete external momenta: active {active:?}, complement {complement:?}, parent {:?}, external count {} (expected {})",
1028 lmb.loop_edges,
1029 external_momenta.len(),
1030 lmb.ext_edges.len(),
1031 ));
1032 }
1033 for index in (0..lmb.loop_edges.len())
1037 .map(LoopIndex)
1038 .filter(|index| !covered.contains(index))
1039 {
1040 if self
1041 .energies
1042 .iter()
1043 .any(|edge| lmb.edge_signatures[*edge].internal[index] != SignOrZero::Zero)
1044 {
1045 return Err(eyre!(
1046 "sampling fiber depends on unsampled parent edge {}",
1047 lmb.loop_edges[index]
1048 ));
1049 }
1050 }
1051 for momentum in external_momenta {
1052 if !momentum.temporal.value.0.is_finite()
1053 || [
1054 &momentum.spatial.px,
1055 &momentum.spatial.py,
1056 &momentum.spatial.pz,
1057 ]
1058 .iter()
1059 .any(|value| !value.0.is_finite())
1060 {
1061 return Err(eyre!("sampling fiber external momenta must be finite"));
1062 }
1063 }
1064 let rows = self
1065 .energies
1066 .iter()
1067 .map(|edge| {
1068 active
1069 .iter()
1070 .map(|&index| match lmb.edge_signatures[*edge].internal[index] {
1071 SignOrZero::Minus => -1_i64,
1072 SignOrZero::Zero => 0,
1073 SignOrZero::Plus => 1,
1074 })
1075 .collect_vec()
1076 })
1077 .collect_vec();
1078 let rank = rank_i64(&rows);
1079 if rank != active.len() {
1080 return Err(eyre!(
1081 "sampling fiber {:?} has active routing rank {rank}, expected {}; spectator directions require a complement block",
1082 self.energies,
1083 active.len()
1084 ));
1085 }
1086 let varying = self
1087 .energies
1088 .iter()
1089 .zip(&rows)
1090 .filter_map(|(&edge, row)| row.iter().any(|&sign| sign != 0).then_some(edge))
1091 .collect_vec();
1092 let subspace_energies = subspace.contains(&self.energies, graph).collect_vec();
1093 if varying.iter().any(|edge| !subspace_energies.contains(edge)) {
1094 return Err(eyre!(
1095 "sampling fiber routing is inconsistent with its selected cycle subgraph"
1096 ));
1097 }
1098 for &edge in &self.energies {
1099 let mass = &masses[edge];
1100 if !mass.0.is_finite() || mass < &mass.zero() {
1101 return Err(eyre!(
1102 "sampling fiber edge {edge} requires a finite nonnegative mass"
1103 ));
1104 }
1105 }
1106 let surface = Arc::new(self.clone());
1107 let lmbs = Arc::new(all_lmbs.clone());
1108 let graph = Arc::new(graph.clone());
1109 let subspace = Arc::new(subspace.clone());
1110 let masses = Arc::new(masses.clone());
1111 let externals = Arc::new(external_momenta.clone());
1112 let settings = Arc::new(settings.clone());
1113 let complement = Arc::new(complement.to_vec());
1114 let active = Arc::new(active);
1115 let zero = external_momenta[ExternalIndex(0)].temporal.value.zero();
1116 let dimension = active.len() * 3;
1117 let freeze_context = complement.is_empty();
1118 let n_loops = lmb.loop_edges.len();
1119 let evaluator = {
1122 let (surface, lmbs, subspace, masses, externals, complement, active) = (
1123 surface.clone(),
1124 lmbs.clone(),
1125 subspace.clone(),
1126 masses.clone(),
1127 externals.clone(),
1128 complement.clone(),
1129 active.clone(),
1130 );
1131 Arc::new(
1132 move |direction: &[T], radius: T, center: &[T], context: &[T]| {
1133 let zero = F(radius.zero());
1134 let mut loops = LoopMomenta::from_iter(
1135 (0..n_loops)
1136 .map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
1137 );
1138 let mut velocity = loops.clone();
1139 for (&index, components) in
1140 complement.iter().zip(context.as_chunks::<3>().0.iter())
1141 {
1142 loops[index] = ThreeMomentum::new(
1143 F(components[0].clone()),
1144 F(components[1].clone()),
1145 F(components[2].clone()),
1146 );
1147 }
1148 for ((&index, components), unit) in active
1149 .iter()
1150 .zip(center.as_chunks::<3>().0.iter())
1151 .zip(direction.as_chunks::<3>().0.iter())
1152 {
1153 loops[index] = ThreeMomentum::new(
1154 F(components[0].clone()),
1155 F(components[1].clone()),
1156 F(components[2].clone()),
1157 );
1158 velocity[index] = ThreeMomentum::new(
1159 F(unit[0].clone()),
1160 F(unit[1].clone()),
1161 F(unit[2].clone()),
1162 );
1163 }
1164 let (value, derivative) = surface.compute_self_and_r_derivative(
1165 &F(radius),
1166 &velocity,
1167 &loops,
1168 &externals,
1169 &masses,
1170 subspace.get_lmb(&lmbs),
1171 );
1172 Ok((value.0, derivative.0))
1173 },
1174 )
1175 };
1176 let preparer = Arc::new(move |context: &[T]| {
1177 let zero = externals[ExternalIndex(0)].temporal.value.zero();
1178 if context.len() != complement.len() * 3
1179 || context.iter().any(|value| !value.is_finite())
1180 {
1181 return Err(eyre!(
1182 "sampling fiber expected {} finite complement components, got {}",
1183 complement.len() * 3,
1184 context.len()
1185 ));
1186 }
1187 let lmb = subspace.get_lmb(&lmbs);
1188 let spatial: ExternalThreeMomenta<F<T>> =
1189 externals.iter().map(|p| p.spatial.clone()).collect();
1190 let mut center = LoopMomenta::from_iter(
1191 (0..n_loops).map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
1192 );
1193 for (&index, components) in complement.iter().zip(context.as_chunks::<3>().0.iter()) {
1194 center[index] = ThreeMomentum::new(
1195 F(components[0].clone()),
1196 F(components[1].clone()),
1197 F(components[2].clone()),
1198 );
1199 }
1200 let zero_velocity = LoopMomenta::from_iter(
1201 (0..n_loops).map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
1202 );
1203 let mut constant = surface.compute_shift_part_from_momenta(&externals, lmb);
1204 let mut scale = center
1208 .iter()
1209 .flat_map(|p| [&p.px, &p.py, &p.pz])
1210 .chain(externals.iter().flat_map(|p| {
1211 [
1212 &p.temporal.value,
1213 &p.spatial.px,
1214 &p.spatial.py,
1215 &p.spatial.pz,
1216 ]
1217 }))
1218 .fold(constant.abs(), |sum, component| sum + component.abs());
1219 for &edge in &surface.energies {
1220 if !varying.contains(&edge) {
1221 let momentum = lmb.edge_signatures[edge].compute_momentum(¢er, &spatial);
1222 let energy = (momentum.norm_squared() + masses[edge].square()).sqrt();
1223 constant += &energy;
1224 scale += energy;
1225 }
1226 }
1227 let minimum = if active.len() == 1 && varying.len() == 2 {
1228 let [first, second] = [varying[0], varying[1]];
1233 let index = active[0];
1234 let sign = |edge| match lmb.edge_signatures[edge].internal[index] {
1235 SignOrZero::Plus => zero.one(),
1236 SignOrZero::Minus => -zero.one(),
1237 SignOrZero::Zero => unreachable!("two varying rank-one energies"),
1238 };
1239 let first_shift = lmb.edge_signatures[first].compute_momentum(¢er, &spatial);
1240 let second_shift = lmb.edge_signatures[second].compute_momentum(¢er, &spatial);
1241 let separation = &first_shift - &(second_shift * (sign(first) * sign(second)));
1242 let total_mass = &masses[first] + &masses[second];
1243 let fraction = if total_mass > zero {
1244 &masses[first] / &total_mass
1245 } else {
1246 zero.one() / zero.from_i64(2)
1247 };
1248 center[index] = (&separation * fraction - first_shift) * sign(first);
1249 let energy = (separation.norm_squared() + total_mass.square()).sqrt();
1250 scale += &energy;
1251 Some(energy + &constant)
1252 } else {
1253 None
1254 };
1255 let tolerance = zero.epsilon()
1256 * zero.from_usize(64 * (surface.energies.len() + 1))
1257 * scale.max(zero.one());
1258 let normalized = |value: &F<T>| -> Result<T> {
1259 let margin = -value / F::<T>::from_f64(settings.kinematics.e_cm);
1260 if !margin.0.is_finite() {
1261 return Err(SamplingEvaluationError::Unrepresentable {
1262 operation: "normalized sampling surface margin",
1263 detail: "finite surface value and energy scale produce a nonfinite margin"
1264 .to_owned(),
1265 }
1266 .into());
1267 }
1268 Ok(margin.0)
1269 };
1270 let status = if let Some(minimum) = minimum {
1271 if !minimum.0.is_finite() || minimum.abs() <= tolerance {
1272 return Err(SamplingEvaluationError::UncertainGeometry { detail: format!("two-energy fiber minimum is not sign-certified: minimum={minimum}, tolerance={tolerance}, edges={:?}", surface.energies) }.into());
1273 }
1274 if minimum > zero {
1275 PreparedSurfaceStatus::absent(format!(
1276 "positive exact two-energy minimum {minimum}"
1277 ))?
1278 } else {
1279 PreparedSurfaceStatus::existing(Some(normalized(&minimum)?))?
1280 }
1281 } else {
1282 let lower = varying
1286 .iter()
1287 .fold(constant, |sum, &edge| sum + &masses[edge]);
1288 if lower > tolerance {
1289 PreparedSurfaceStatus::absent(format!("positive energy lower bound {lower}"))?
1290 } else {
1291 let value = surface
1292 .compute_self_and_r_derivative(
1293 &zero,
1294 &zero_velocity,
1295 ¢er,
1296 &externals,
1297 &masses,
1298 lmb,
1299 )
1300 .0;
1301 if !esurface_value_is_strictly_inside(
1302 &value,
1303 &F::<T>::from_f64(settings.kinematics.e_cm),
1304 ) {
1305 let thresholds = EsurfaceCollection::from(vec![surface.as_ref().clone()]);
1306 let existing = ExistingThresholds::from(vec![EsurfaceID(0)]);
1307 let input = OverlapInput {
1308 graph: &graph,
1309 settings: &settings,
1310 subspace: &subspace,
1311 threshold_subspaces: None,
1312 lmbs: &lmbs,
1313 thresholds: &thresholds,
1314 edge_masses: masses.iter().map(|(_, mass)| mass.into_ff64()).collect(),
1315 surface_kinematics: None,
1316 };
1317 let loops: LoopMomenta<F<f64>> =
1318 center.iter().map(ThreeMomentum::to_f64).collect();
1319 let external: ExternalFourMomenta<F<f64>> =
1320 externals.iter().map(|p| p.to_f64()).collect();
1321 if loops
1322 .iter()
1323 .flat_map(|p| [&p.px, &p.py, &p.pz])
1324 .any(|v| !v.0.is_finite())
1325 || external
1326 .iter()
1327 .flat_map(|p| {
1328 [
1329 &p.temporal.value,
1330 &p.spatial.px,
1331 &p.spatial.py,
1332 &p.spatial.pz,
1333 ]
1334 })
1335 .any(|v| !v.0.is_finite())
1336 || input
1337 .edge_masses
1338 .iter()
1339 .any(|(_, mass)| !mass.0.is_finite())
1340 {
1341 return Err(SamplingEvaluationError::UncertainGeometry {
1342 detail: "native fiber data exceed the finite range of the approximate f64 SOCP center seed".to_owned(),
1343 }.into());
1344 }
1345 let candidate = find_center(&input, &[ExistingEsurfaceId(0)], &existing, &loops, &external, false)
1346 .map_err(|error| SamplingEvaluationError::UncertainGeometry { detail: format!("fiber center seed failed without an absence certificate: {error}") })?
1347 .ok_or_else(|| SamplingEvaluationError::UncertainGeometry { detail: "fiber SOCP reported infeasibility without a native absence certificate".to_owned() })?;
1348 for &index in active.iter() {
1349 center[index] = ThreeMomentum::from_ff64(candidate[index]);
1350 }
1351 }
1352 let value = surface
1353 .compute_self_and_r_derivative(
1354 &zero,
1355 &zero_velocity,
1356 ¢er,
1357 &externals,
1358 &masses,
1359 lmb,
1360 )
1361 .0;
1362 if !esurface_value_is_strictly_inside(
1363 &value,
1364 &F::<T>::from_f64(settings.kinematics.e_cm),
1365 ) {
1366 return Err(SamplingEvaluationError::UncertainGeometry {
1367 detail: format!(
1368 "fiber center is not natively certified inside: value={value}"
1369 ),
1370 }
1371 .into());
1372 }
1373 PreparedSurfaceStatus::existing(Some(normalized(&value)?))?
1374 }
1375 };
1376 if status.is_existing() {
1377 let value = surface
1380 .compute_self_and_r_derivative(
1381 &zero,
1382 &zero_velocity,
1383 ¢er,
1384 &externals,
1385 &masses,
1386 lmb,
1387 )
1388 .0;
1389 if !esurface_value_is_strictly_inside(
1390 &value,
1391 &F::<T>::from_f64(settings.kinematics.e_cm),
1392 ) || value >= -&tolerance
1393 {
1394 return Err(SamplingEvaluationError::UncertainGeometry {
1395 detail: format!(
1396 "prepared fiber center is not natively certified inside: value={value}, tolerance={tolerance}"
1397 ),
1398 }
1399 .into());
1400 }
1401 }
1402 let center = active
1403 .iter()
1404 .flat_map(|&index| {
1405 [
1406 center[index].px.0.clone(),
1407 center[index].py.0.clone(),
1408 center[index].pz.0.clone(),
1409 ]
1410 })
1411 .collect();
1412 Ok((center, status))
1413 });
1414 ImplicitSurfaceRadialMap::new(
1415 dimension,
1416 vec![zero.0; dimension],
1417 beta,
1418 power,
1419 Arc::new(|_, _| Err(eyre!("sampling fiber requires prepared complement data"))),
1420 )
1421 .and_then(|map| {
1422 let map = map
1423 .with_context_evaluator(evaluator)
1424 .with_context_preparer(preparer);
1425 if freeze_context {
1426 map.freeze_context(&[])
1427 } else {
1428 Ok(map)
1429 }
1430 })
1431 }
1432
1433 pub(crate) fn has_radial_dependence_in_subspace(
1434 &self,
1435 subspace: &SubspaceData,
1436 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1437 graph: &Graph,
1438 ) -> bool {
1439 let lmb = subspace.get_lmb(all_lmbs);
1440 subspace.contains(&self.energies, graph).any(|index| {
1441 subspace
1442 .project_loop_signature(&lmb.edge_signatures[index].internal)
1443 .any(|sign| sign.is_sign())
1444 })
1445 }
1446
1447 pub(crate) fn external_shift_is_strictly_negative_for_positive_energies(
1448 &self,
1449 incoming_edges: &[EdgeIndex],
1450 outgoing_edges: &[EdgeIndex],
1451 ) -> bool {
1452 if incoming_edges.is_empty()
1453 || outgoing_edges.is_empty()
1454 || incoming_edges
1455 .iter()
1456 .any(|edge| outgoing_edges.contains(edge))
1457 || self
1458 .external_shift
1459 .iter()
1460 .any(|(edge, _)| !incoming_edges.contains(edge) && !outgoing_edges.contains(edge))
1461 {
1462 return false;
1463 }
1464
1465 let coefficient = |edge: &EdgeIndex| {
1466 self.external_shift
1467 .iter()
1468 .filter(|(shift_edge, _)| shift_edge == edge)
1469 .map(|(_, coefficient)| i128::from(*coefficient))
1470 .sum::<i128>()
1471 };
1472
1473 let lambda_lower_bound = outgoing_edges
1479 .iter()
1480 .map(&coefficient)
1481 .max()
1482 .expect("outgoing external edges were checked to be non-empty");
1483 let lambda_upper_bound = incoming_edges
1484 .iter()
1485 .map(|edge| -coefficient(edge))
1486 .min()
1487 .expect("incoming external edges were checked to be non-empty");
1488
1489 if lambda_lower_bound > lambda_upper_bound {
1490 return false;
1491 }
1492
1493 let lambda = lambda_lower_bound;
1494 let adjusted_coefficients = incoming_edges
1495 .iter()
1496 .map(|edge| coefficient(edge) + lambda)
1497 .chain(outgoing_edges.iter().map(|edge| coefficient(edge) - lambda));
1498 let mut has_strictly_negative_coefficient = false;
1499 for adjusted_coefficient in adjusted_coefficients {
1500 if adjusted_coefficient > 0 {
1501 return false;
1502 }
1503 has_strictly_negative_coefficient |= adjusted_coefficient < 0;
1504 }
1505
1506 has_strictly_negative_coefficient
1507 }
1508
1509 pub(crate) fn to_atom(&self, cut_edges: &[EdgeIndex]) -> Atom {
1510 let symbolic_energies = self
1511 .energies
1512 .iter()
1513 .map(|i| {
1514 if cut_edges.contains(i) {
1515 cut_energy(*i)
1516 } else {
1517 ose_atom_from_index(*i)
1518 }
1519 })
1520 .collect_vec();
1521
1522 let symbolic_shift = self
1523 .external_shift
1524 .iter()
1525 .fold(Atom::new(), |sum, (i, sign)| {
1526 external_energy_atom_from_index(*i) * &Atom::num(*sign) + &sum
1527 });
1528
1529 let builder_atom = Atom::new();
1530 let energy_sum = symbolic_energies
1531 .iter()
1532 .fold(builder_atom, |acc, energy| acc + energy);
1533
1534 energy_sum + &symbolic_shift
1535 }
1536
1537 #[inline]
1538 pub(crate) fn compute_from_dual_momenta<T: FloatLike>(
1539 &self,
1540 lmb: &LoopMomentumBasis,
1541 real_mass_vector: &EdgeVec<F<T>>,
1542 dual_loop_moms: &LoopMomenta<HyperDual<F<T>>>,
1543 dual_external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
1544 ) -> HyperDual<F<T>> {
1545 let spatial_part_of_externals = dual_external_moms
1546 .iter()
1547 .map(|mom| mom.spatial.clone())
1548 .collect::<TiVec<ExternalIndex, _>>();
1549
1550 let energy_sum = self
1551 .energies
1552 .iter()
1553 .map(|index| {
1554 let signature = &lmb.edge_signatures[*index];
1555 let momentum = signature
1556 .try_compute_momentum(&dual_loop_moms.0, &spatial_part_of_externals.raw)
1557 .unwrap_or_else(|| unreachable!());
1558 let mass = &real_mass_vector[*index];
1559
1560 (momentum.norm_squared() + mass * mass).sqrt()
1561 })
1562 .reduce(|acc, x| acc + x)
1563 .unwrap_or_else(|| dual_loop_moms[LoopIndex(0)].px.zero());
1564
1565 let shift_part = self
1566 .external_shift
1567 .iter()
1568 .map(|(index, sign)| {
1569 let external_signature = &lmb.edge_signatures[*index].external;
1570 let sign = energy_sum.values[0].from_i64(*sign);
1571 new_constant(&energy_sum, &sign)
1572 * external_signature
1573 .try_apply(&dual_external_moms.raw)
1574 .map(|mom| mom.temporal.value)
1575 .unwrap_or_else(|| energy_sum.zero())
1576 })
1577 .reduce(|acc, x| acc + x)
1578 .unwrap_or_else(|| energy_sum.zero());
1579
1580 energy_sum + shift_part
1581 }
1582
1583 #[inline]
1586 pub(crate) fn compute_from_momenta<T: FloatLike>(
1587 &self,
1588 lmb: &LoopMomentumBasis,
1589 real_mass_vector: &EdgeVec<F<T>>,
1590 loop_moms: &LoopMomenta<F<T>>,
1591 external_moms: &ExternalFourMomenta<F<T>>,
1592 ) -> F<T> {
1593 let spatial_part_of_externals = external_moms
1594 .iter()
1595 .map(|mom| mom.spatial.clone())
1596 .collect::<TiVec<ExternalIndex, _>>();
1597
1598 let energy_sum = self
1599 .energies
1600 .iter()
1601 .map(|index| {
1602 let signature = &lmb.edge_signatures[*index];
1603 let momentum = signature.compute_momentum(loop_moms, &spatial_part_of_externals);
1604 let mass = &real_mass_vector[*index];
1605
1606 (momentum.norm_squared() + mass * mass).sqrt()
1607 })
1608 .reduce(|acc, x| acc + x)
1609 .unwrap_or_else(|| loop_moms[LoopIndex(0)].px.zero());
1610
1611 let shift_part = self
1612 .external_shift
1613 .iter()
1614 .map(|(index, sign)| {
1615 let external_signature = &lmb.edge_signatures[*index].external;
1616 energy_sum.from_i64(*sign)
1617 * compute_t_part_of_shift_part(external_signature, external_moms)
1618 })
1619 .reduce(|acc, x| acc + x)
1620 .unwrap_or_else(|| energy_sum.zero());
1621
1622 energy_sum + shift_part
1623 }
1624
1625 fn classify_invariant_margin<T: FloatLike>(
1626 shift_part: &F<T>,
1627 invariant_margin: F<T>,
1628 e_cm: &F<T>,
1629 normalized_margin_tolerance: &F<T>,
1630 ) -> EsurfaceExistence<T> {
1631 let normalized_margin_tolerance =
1644 if normalized_margin_tolerance.is_nan() || normalized_margin_tolerance.is_infinite() {
1645 F::from_f64(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD)
1646 } else {
1647 normalized_margin_tolerance.abs()
1648 };
1649 let invariant_tolerance = &normalized_margin_tolerance * e_cm * e_cm;
1650 let normalized_margin = &invariant_margin / (e_cm * e_cm);
1651 let shift_tolerance = F::from_f64(ESURFACE_SHIFT_THRESHOLD) * e_cm;
1652
1653 if invariant_margin.abs() <= invariant_tolerance && shift_part <= &shift_tolerance {
1654 return EsurfaceExistence::Pinched { normalized_margin };
1655 }
1656
1657 if shift_part >= &(-shift_tolerance) {
1658 return EsurfaceExistence::NonExisting {
1659 normalized_margin: Some(normalized_margin),
1660 reason: NonExistingEsurfaceReason::ShiftNotNegative,
1661 };
1662 }
1663
1664 if invariant_margin > invariant_tolerance {
1665 EsurfaceExistence::Existing { normalized_margin }
1666 } else if invariant_margin >= -invariant_tolerance {
1667 EsurfaceExistence::Pinched { normalized_margin }
1668 } else {
1669 EsurfaceExistence::NonExisting {
1670 normalized_margin: Some(normalized_margin),
1671 reason: NonExistingEsurfaceReason::NoRealZero,
1672 }
1673 }
1674 }
1675
1676 #[inline]
1677 #[allow(clippy::too_many_arguments)]
1678 pub(crate) fn classify_existence_subspace<T: FloatLike>(
1681 &self,
1682 loop_moms: &LoopMomenta<F<T>>,
1683 external_moms: &ExternalFourMomenta<F<T>>,
1684 subspace: &SubspaceData,
1685 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1686 graph: &Graph,
1687 real_mass_vector: &EdgeVec<F<T>>,
1688 reversed_edges: &[EdgeIndex],
1689 e_cm: &F<T>,
1690 normalized_margin_tolerance: &F<T>,
1691 ) -> EsurfaceExistence<T> {
1692 if self.external_shift.is_empty() {
1694 debug!("esurface has no external shift, cannot exist");
1695 return EsurfaceExistence::NonExisting {
1696 normalized_margin: None,
1697 reason: NonExistingEsurfaceReason::NoExternalShift,
1698 };
1699 }
1700
1701 let shift_part = self.compute_shift_part_from_momenta_in_subspace(
1702 loop_moms,
1703 external_moms,
1704 subspace,
1705 all_lmbs,
1706 graph,
1707 real_mass_vector,
1708 );
1709
1710 let subspace_energy_indices = subspace.contains(&self.energies, graph).collect_vec();
1711
1712 if !self.has_radial_dependence_in_subspace(subspace, all_lmbs, graph) {
1713 debug!(
1714 "esurface has no radial energy in this subspace, cannot bound a threshold region"
1715 );
1716 return EsurfaceExistence::NonExisting {
1717 normalized_margin: None,
1718 reason: NonExistingEsurfaceReason::NoRadialDependence,
1719 };
1720 }
1721
1722 let lmb = subspace.get_lmb(all_lmbs);
1723 let mass_sum: F<T> = subspace_energy_indices
1724 .iter()
1725 .map(|&index| &real_mass_vector[index])
1726 .fold(F::from_f64(0.0), |acc, x| acc + x);
1727
1728 let zero_vector = ThreeMomentum::new(e_cm.zero(), e_cm.zero(), e_cm.zero());
1729
1730 let graph_vector = self
1731 .external_shift
1732 .iter()
1733 .map(|(index, sign)| {
1734 let external_signature = &lmb.edge_signatures[*index].external;
1735 compute_shift_part(external_signature, external_moms).spatial
1736 * F::from_f64(*sign as f64)
1737 })
1738 .reduce(|acc, x| acc + x)
1739 .unwrap_or_else(|| zero_vector.clone());
1740
1741 let other_part = subspace
1742 .does_not_contain(&self.energies, graph)
1743 .map(|index| {
1744 let signature = &lmb.edge_signatures[index];
1745 let sign = if reversed_edges.contains(&index) {
1746 -F::from_f64(1.0)
1747 } else {
1748 F::from_f64(1.0)
1749 };
1750
1751 signature.compute_momentum(
1752 loop_moms,
1753 &external_moms
1754 .iter()
1755 .map(|mom| mom.spatial.clone())
1756 .collect::<TiVec<ExternalIndex, _>>(),
1757 ) * sign
1758 })
1759 .reduce(|acc, x| acc + x)
1760 .unwrap_or_else(|| zero_vector.clone());
1761
1762 let shift_vector_sq = (&graph_vector + &other_part).norm_squared();
1763 let invariant_margin = &shift_part * &shift_part - &shift_vector_sq - &mass_sum * &mass_sum;
1764 let classification = Self::classify_invariant_margin(
1765 &shift_part,
1766 invariant_margin,
1767 e_cm,
1768 normalized_margin_tolerance,
1769 );
1770
1771 if !classification.is_existing() {
1772 debug!(
1773 "subspace esurface classified as {}: shift_part^2: {}, shift_vector_sq: {}, mass_sum^2: {}, normalized_margin: {:?}",
1774 classification.label(),
1775 &shift_part * &shift_part,
1776 shift_vector_sq,
1777 &mass_sum * &mass_sum,
1778 classification.normalized_margin(),
1779 );
1780 }
1781
1782 classification
1783 }
1784
1785 #[inline]
1786 pub(crate) fn classify_existence<T: FloatLike>(
1789 &self,
1790 external_moms: &ExternalFourMomenta<F<T>>,
1791 lmb: &LoopMomentumBasis,
1792 real_mass_vector: &EdgeVec<F<T>>,
1793 e_cm: &F<T>,
1794 normalized_margin_tolerance: &F<T>,
1795 ) -> EsurfaceExistence<T> {
1796 if self.external_shift.is_empty() {
1798 return EsurfaceExistence::NonExisting {
1799 normalized_margin: None,
1800 reason: NonExistingEsurfaceReason::NoExternalShift,
1801 };
1802 }
1803
1804 let shift_part = self.compute_shift_part_from_momenta(external_moms, lmb);
1805 let mass_sum: F<T> = self
1806 .energies
1807 .iter()
1808 .map(|index| &real_mass_vector[*index])
1809 .fold(F::from_f64(0.0), |acc, x| acc + x);
1810
1811 let zero_vector = ThreeMomentum::new(e_cm.zero(), e_cm.zero(), e_cm.zero());
1812
1813 let shift_vector = self
1814 .external_shift
1815 .iter()
1816 .map(|(index, sign)| {
1817 let external_signature = &lmb.edge_signatures[*index].external;
1818 compute_shift_part(external_signature, external_moms).spatial
1819 * F::from_f64(*sign as f64)
1820 })
1821 .reduce(|acc, x| acc + x)
1822 .unwrap_or_else(|| zero_vector.clone());
1823
1824 let shift_vector_sq = shift_vector.norm_squared();
1825 let invariant_margin = &shift_part * &shift_part - shift_vector_sq - &mass_sum * &mass_sum;
1826
1827 Self::classify_invariant_margin(
1828 &shift_part,
1829 invariant_margin,
1830 e_cm,
1831 normalized_margin_tolerance,
1832 )
1833 }
1834
1835 pub fn existence_status<T: FloatLike>(
1838 &self,
1839 external_moms: &ExternalFourMomenta<F<T>>,
1840 lmb: &LoopMomentumBasis,
1841 real_mass_vector: &EdgeVec<F<T>>,
1842 e_cm: &F<T>,
1843 normalized_margin_tolerance: &F<T>,
1844 ) -> EsurfaceExistenceStatus {
1845 self.classify_existence(
1846 external_moms,
1847 lmb,
1848 real_mass_vector,
1849 e_cm,
1850 normalized_margin_tolerance,
1851 )
1852 .status()
1853 }
1854
1855 pub(crate) fn compute_shift_part_from_momenta_in_subspace<T: FloatLike>(
1857 &self,
1858 loop_moms: &LoopMomenta<F<T>>,
1859 external_moms: &ExternalFourMomenta<F<T>>,
1860 subspace: &SubspaceData,
1861 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1862 graph: &Graph,
1863 masses: &EdgeVec<F<T>>,
1864 ) -> F<T> {
1865 let lmb = subspace.get_lmb(all_lmbs);
1866
1867 let full_external_shift = self
1868 .external_shift
1869 .iter()
1870 .map(|(index, sign)| {
1871 let external_signature = &lmb.edge_signatures[*index].external;
1872 external_moms[ExternalIndex(0)]
1873 .temporal
1874 .value
1875 .from_i64(*sign)
1876 * compute_t_part_of_shift_part(external_signature, external_moms)
1877 })
1878 .reduce(|acc, x| acc + x)
1879 .unwrap_or_else(|| external_moms[ExternalIndex(0)].temporal.value.zero());
1880
1881 let spatial_externals = external_moms
1882 .iter()
1883 .map(|mom| mom.spatial.clone())
1884 .collect::<TiVec<ExternalIndex, _>>();
1885
1886 let remaining_shift = subspace
1887 .does_not_contain(&self.energies, graph)
1888 .map(|index| {
1889 let signature = &lmb.edge_signatures[index];
1890 let momentum = signature.compute_momentum(loop_moms, &spatial_externals);
1892 let mass = &masses[index];
1893
1894 (momentum.norm_squared() + mass * mass).sqrt()
1895 })
1896 .reduce(|acc, x| acc + x)
1897 .unwrap_or_else(|| full_external_shift.zero());
1898
1899 full_external_shift + remaining_shift
1900 }
1901
1902 pub(crate) fn compute_shift_part_from_momenta<T: FloatLike>(
1904 &self,
1905 external_moms: &ExternalFourMomenta<F<T>>,
1906 lmb: &LoopMomentumBasis,
1907 ) -> F<T> {
1908 self.external_shift
1909 .iter()
1910 .map(|(index, sign)| {
1911 let external_signature = &lmb.edge_signatures[*index].external;
1912 external_moms[ExternalIndex(0)]
1913 .temporal
1914 .value
1915 .from_i64(*sign)
1916 * compute_t_part_of_shift_part(external_signature, external_moms)
1917 })
1918 .reduce(|acc, x| acc + x)
1919 .unwrap_or_else(|| external_moms[ExternalIndex(0)].temporal.value.zero())
1920 }
1921
1922 #[inline]
1923 #[allow(clippy::too_many_arguments)]
1924 pub(crate) fn compute_self_and_r_derivative_subspace<T: FloatLike>(
1925 &self,
1926 radius: &F<T>,
1927 shifted_unit_loops_in_subspace: &LoopMomenta<F<T>>,
1928 center_in_subspace: &LoopMomenta<F<T>>,
1929 external_moms: &ExternalFourMomenta<F<T>>,
1930 real_mass_vector: &EdgeVec<F<T>>,
1931 subspace: &SubspaceData,
1932 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1933 graph: &Graph,
1934 ) -> (F<T>, F<T>) {
1935 let spatial_part_of_externals: ExternalThreeMomenta<F<T>> = external_moms
1936 .iter()
1937 .map(|mom| mom.spatial.clone())
1938 .collect();
1939
1940 let loops: LoopMomenta<F<T>> = shifted_unit_loops_in_subspace
1941 .iter_enumerated()
1942 .map(|(loop_index, shifted_unit_momenta)| {
1943 if subspace.contains_loop_index(loop_index) {
1944 shifted_unit_momenta * radius + ¢er_in_subspace[loop_index]
1945 } else {
1946 shifted_unit_momenta.clone()
1947 }
1948 })
1949 .collect();
1950
1951 let shift = self.compute_shift_part_from_momenta_in_subspace(
1952 shifted_unit_loops_in_subspace,
1953 external_moms,
1954 subspace,
1955 all_lmbs,
1956 graph,
1957 real_mass_vector,
1958 );
1959
1960 let lmb = subspace.get_lmb(all_lmbs);
1961 let (derivative, energy_sum) = subspace
1962 .contains(&self.energies, graph)
1963 .map(|index| {
1964 let signature = &lmb.edge_signatures[index];
1965
1966 let momentum = signature.compute_momentum(&loops, &spatial_part_of_externals);
1967 let unit_loop_part = compute_loop_part_subspace(
1968 &signature.internal,
1969 shifted_unit_loops_in_subspace,
1970 subspace,
1971 );
1972
1973 let energy = (momentum.norm_squared()
1974 + &real_mass_vector[index] * &real_mass_vector[index])
1975 .sqrt();
1976
1977 let numerator = momentum * &unit_loop_part;
1978
1979 (numerator / &energy, energy)
1980 })
1981 .fold(
1982 (radius.zero(), radius.zero()),
1983 |(der_sum, en_sum), (der, en)| (der_sum + der, en_sum + en),
1984 );
1985
1986 (energy_sum + shift, derivative)
1987 }
1988
1989 #[inline]
1991 pub(crate) fn compute_self_and_r_derivative<T: FloatLike>(
1992 &self,
1993 radius: &F<T>,
1994 shifted_unit_loops: &LoopMomenta<F<T>>,
1995 center: &LoopMomenta<F<T>>,
1996 external_moms: &ExternalFourMomenta<F<T>>,
1997 real_mass_vector: &EdgeVec<F<T>>,
1998 lmb: &LoopMomentumBasis,
1999 ) -> (F<T>, F<T>) {
2000 let spatial_part_of_externals: ExternalThreeMomenta<F<T>> = external_moms
2001 .iter()
2002 .map(|mom| mom.spatial.clone())
2003 .collect();
2004
2005 let loops: LoopMomenta<F<T>> = shifted_unit_loops
2006 .iter()
2007 .zip(center.iter())
2008 .map(|(momentum, center)| momentum * radius + center)
2009 .collect();
2010
2011 let shift = self.compute_shift_part_from_momenta(external_moms, lmb);
2012
2013 let (derivative, energy_sum) = self
2014 .energies
2015 .iter()
2016 .map(|&index| {
2017 let signature = &lmb.edge_signatures[index];
2018
2019 let momentum = signature.compute_momentum(&loops, &spatial_part_of_externals);
2020 let unit_loop_part = compute_loop_part(&signature.internal, shifted_unit_loops);
2021
2022 let energy = (momentum.norm_squared()
2023 + &real_mass_vector[index] * &real_mass_vector[index])
2024 .sqrt();
2025
2026 let zero = radius.zero();
2031 let derivative = if radius == &zero
2032 && real_mass_vector[index] == zero
2033 && momentum.px == zero
2034 && momentum.py == zero
2035 && momentum.pz == zero
2036 {
2037 unit_loop_part.norm_squared().sqrt()
2038 } else {
2039 momentum * &unit_loop_part / &energy
2040 };
2041
2042 (derivative, energy)
2043 })
2044 .fold(
2045 (radius.zero(), radius.zero()),
2046 |(der_sum, en_sum), (der, en)| (der_sum + der, en_sum + en),
2047 );
2048
2049 (energy_sum + shift, derivative)
2050 }
2051
2052 pub(crate) fn evaluate_routed_enclosed<T: FloatLike>(
2057 &self,
2058 radius: &F<T>,
2059 loop_momenta: &LoopMomenta<F<T>>,
2060 external_momenta: &ExternalFourMomenta<F<T>>,
2061 masses: &EdgeVec<F<T>>,
2062 lmb: &LoopMomentumBasis,
2063 ) -> Result<[Float; 2]> {
2064 use linnet::num_traits::SignOrZero;
2065 if loop_momenta.len() != lmb.loop_edges.len()
2066 || external_momenta.len() != lmb.ext_edges.len()
2067 {
2068 return Err(eyre!(
2069 "routed E-surface enclosure requires the complete parent and external frame"
2070 ));
2071 }
2072 if !radius.0.is_finite()
2073 || loop_momenta
2074 .iter()
2075 .flat_map(|p| [&p.px, &p.py, &p.pz])
2076 .chain(external_momenta.iter().flat_map(|p| {
2077 [
2078 &p.temporal.value,
2079 &p.spatial.px,
2080 &p.spatial.py,
2081 &p.spatial.pz,
2082 ]
2083 }))
2084 .any(|x| !x.0.is_finite())
2085 {
2086 return Err(SamplingEvaluationError::Unrepresentable {
2087 operation: "routed E-surface enclosure",
2088 detail: "nonfinite original radius or momentum component".into(),
2089 }
2090 .into());
2091 }
2092 for edge in self
2093 .energies
2094 .iter()
2095 .chain(self.external_shift.iter().map(|(edge, _)| edge))
2096 {
2097 let signature = lmb.edge_signatures.get(*edge).ok_or_else(|| {
2098 eyre!("routed E-surface enclosure has no signature for edge {edge}")
2099 })?;
2100 if (!signature.internal.is_empty() && signature.internal.len() != loop_momenta.len())
2103 || (!signature.external.is_empty()
2104 && signature.external.len() != external_momenta.len())
2105 {
2106 return Err(eyre!(
2107 "routed E-surface enclosure has an incomplete signature for edge {edge}"
2108 ));
2109 }
2110 }
2111 let coefficient = |sign: &SignOrZero| match sign {
2112 SignOrZero::Plus => 1,
2113 SignOrZero::Minus => -1,
2114 SignOrZero::Zero => 0,
2115 };
2116 let spatial = |p: &ThreeMomentum<F<T>>, axis| {
2117 EsurfaceRay::<T>::enclosed_native([&p.px, &p.py, &p.pz][axis])
2118 };
2119 let mut energies = Vec::with_capacity(self.energies.len());
2120 for &edge in &self.energies {
2121 let mass = masses
2122 .get(edge)
2123 .ok_or_else(|| eyre!("routed E-surface enclosure has no mass for edge {edge}"))?;
2124 if !mass.0.is_finite() {
2125 return Err(SamplingEvaluationError::Unrepresentable {
2126 operation: "routed E-surface enclosure",
2127 detail: format!("nonfinite original mass on edge {edge}"),
2128 }
2129 .into());
2130 }
2131 let signature = &lmb.edge_signatures[edge];
2132 let velocity = std::array::from_fn(|axis| {
2133 EsurfaceRay::<T>::enclosed_sum(
2134 signature
2135 .internal
2136 .iter()
2137 .zip(loop_momenta.iter())
2138 .map(|(sign, p)| (coefficient(sign), spatial(p, axis))),
2139 )
2140 });
2141 let offset = std::array::from_fn(|axis| {
2142 EsurfaceRay::<T>::enclosed_sum(
2143 signature
2144 .external
2145 .iter()
2146 .zip(external_momenta.iter())
2147 .map(|(sign, p)| (coefficient(sign), spatial(&p.spatial, axis))),
2148 )
2149 });
2150 energies.push((velocity, offset, EsurfaceRay::<T>::enclosed_native(mass)));
2151 }
2152 let shift =
2153 EsurfaceRay::<T>::enclosed_sum(self.external_shift.iter().map(|(edge, scale)| {
2154 let temporal = EsurfaceRay::<T>::enclosed_sum(
2155 lmb.edge_signatures[*edge]
2156 .external
2157 .iter()
2158 .zip(external_momenta.iter())
2159 .map(|(sign, p)| {
2160 (
2161 coefficient(sign),
2162 EsurfaceRay::<T>::enclosed_native(&p.temporal.value),
2163 )
2164 }),
2165 );
2166 (*scale, temporal)
2167 }));
2168 Ok(EsurfaceRay::<T>::evaluate_enclosed(
2169 energies.into_iter(),
2170 shift,
2171 &EsurfaceRay::<T>::enclosed_native(radius),
2172 false,
2173 )?
2174 .0)
2175 }
2176
2177 pub(crate) fn routed_ray<T: FloatLike>(
2182 &self,
2183 velocity: &LoopMomenta<F<T>>,
2184 center: &LoopMomenta<F<T>>,
2185 external_moms: &ExternalFourMomenta<F<T>>,
2186 masses: &EdgeVec<F<T>>,
2187 lmb: &LoopMomentumBasis,
2188 ) -> EsurfaceRay<T> {
2189 let spatial: ExternalThreeMomenta<F<T>> = external_moms
2190 .iter()
2191 .map(|momentum| momentum.spatial.clone())
2192 .collect();
2193 EsurfaceRay {
2194 energies: self
2195 .energies
2196 .iter()
2197 .map(|&edge| {
2198 let signature = &lmb.edge_signatures[edge];
2199 (
2200 edge,
2201 compute_loop_part(&signature.internal, velocity),
2202 signature.compute_momentum(center, &spatial),
2203 masses[edge].clone(),
2204 )
2205 })
2206 .collect(),
2207 shift: self.compute_shift_part_from_momenta(external_moms, lmb),
2208 }
2209 }
2210
2211 #[allow(clippy::too_many_arguments)]
2215 pub(crate) fn solve_lu_cut<T: FloatLike>(
2216 &self,
2217 loop_momenta: &LoopMomenta<F<T>>,
2218 external_momenta: &ExternalFourMomenta<F<T>>,
2219 masses: &EdgeVec<F<T>>,
2220 lmb: &LoopMomentumBasis,
2221 e_cm: &F<T>,
2222 diagnostics: &mut RadialRootDiagnostics,
2223 identity: &RadialRootIdentity,
2224 ) -> std::result::Result<(EsurfaceRay<T>, NewtonIterationResult<T>), SafeguardedNewtonError<T>>
2225 {
2226 let zero = loop_momenta[LoopIndex(0)].px.zero();
2227 let center = LoopMomenta::from_iter(
2228 (0..loop_momenta.len())
2229 .map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
2230 );
2231 let ray = self.routed_ray(loop_momenta, ¢er, external_momenta, masses, lmb);
2232 let solution = ray.solve_lu_cut(e_cm, diagnostics, identity)?;
2233 Ok((ray, solution))
2234 }
2235
2236 pub(crate) fn get_radius_guess_subspace<T: FloatLike>(
2240 &self,
2241 loops_unit_in_subspace: &LoopMomenta<F<T>>,
2242 external_moms: &ExternalFourMomenta<F<T>>,
2243 subspace: &SubspaceData,
2244 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2245 graph: &Graph,
2246 masses: &EdgeVec<F<T>>,
2247 ) -> (F<T>, F<T>) {
2248 let const_builder = &loops_unit_in_subspace[LoopIndex(0)].px;
2249
2250 let esurface_shift = self.compute_shift_part_from_momenta_in_subspace(
2251 loops_unit_in_subspace,
2252 external_moms,
2253 subspace,
2254 all_lmbs,
2255 graph,
2256 masses,
2257 );
2258
2259 debug!("shift part for radius guess: {}", esurface_shift);
2260
2261 let mut radius_guess = const_builder.zero();
2262 let mut denominator = const_builder.zero();
2263
2264 let lmb = subspace.get_lmb(all_lmbs);
2265
2266 debug!("unit loops in subspace: {:?}", loops_unit_in_subspace);
2267 let external_3_momenta = external_moms.iter().map(|x| x.spatial.clone()).collect();
2268
2269 for energy in subspace.contains(&self.energies, graph) {
2271 debug!("computing contribution for energy {:?}", energy);
2272 let signature = &lmb.edge_signatures[energy];
2273 let unit_loop_part =
2276 compute_loop_part_subspace(&signature.internal, loops_unit_in_subspace, subspace);
2277 let three_shift = compute_shift_part_subspace(
2280 &signature.internal,
2281 &signature.external,
2282 loops_unit_in_subspace,
2283 &external_3_momenta,
2284 subspace,
2285 );
2286 let norm_unit_loop_part_squared = unit_loop_part.norm_squared();
2289 if norm_unit_loop_part_squared == const_builder.zero() {
2292 continue;
2293 }
2294 let loop_dot_shift = &unit_loop_part * three_shift;
2295
2296 debug!(
2297 "norm unit loop part squared: {}",
2298 norm_unit_loop_part_squared
2299 );
2300
2301 radius_guess += loop_dot_shift.abs() / &norm_unit_loop_part_squared;
2302 debug!("current radius guess: {}", radius_guess);
2303 denominator += norm_unit_loop_part_squared.sqrt();
2304 debug!("current denominator: {}", denominator);
2305 }
2306
2307 if denominator != const_builder.zero() {
2308 radius_guess += esurface_shift.abs() / denominator;
2309 }
2310 debug!("final radius guess: {}", radius_guess);
2311 let negative_radius = radius_guess.ref_neg();
2312 (radius_guess, negative_radius)
2313 }
2314
2315 pub(crate) fn get_radius_guess<T: FloatLike>(
2317 &self,
2318 unit_loops: &LoopMomenta<F<T>>,
2319 external_moms: &ExternalFourMomenta<F<T>>,
2320 lmb: &LoopMomentumBasis,
2321 ) -> (F<T>, F<T>) {
2322 let shift = self.compute_shift_part_from_momenta(external_moms, lmb);
2323 let terms = self.energies.iter().map(|&energy| {
2325 let signature = &lmb.edge_signatures[energy];
2327 let unit_loop_part = compute_loop_part(&signature.internal, unit_loops);
2329 let shift = compute_shift_part(&signature.external, external_moms).spatial;
2331 (unit_loop_part.norm_squared(), &unit_loop_part * shift)
2333 });
2334 let guess = Self::radius_guess_from_terms(&shift, terms);
2337 let negative = guess.ref_neg();
2338 (guess, negative)
2339 }
2340
2341 fn radius_guess_from_terms<T: FloatLike>(
2342 shift: &F<T>,
2343 terms: impl Iterator<Item = (F<T>, F<T>)>,
2344 ) -> F<T> {
2345 let zero = shift.zero();
2346 let mut guess = zero.clone();
2347 let mut denominator = zero.clone();
2348 for (norm_squared, dot_offset) in terms {
2351 if norm_squared == zero {
2353 continue;
2354 }
2355 guess += dot_offset.abs() / &norm_squared;
2356 denominator += norm_squared.sqrt();
2357 }
2358 if denominator != zero {
2359 guess += shift.abs() / denominator;
2360 }
2361 guess
2362 }
2363
2364 pub(crate) fn canonicalize_shift(&mut self, shift_rewrite: &ShiftRewrite) {
2365 if let Some(dep_mom_pos) = self
2366 .external_shift
2367 .iter()
2368 .position(|(index, _)| *index == shift_rewrite.dependent_momentum)
2369 {
2370 let (_, dep_mom_sign) = self.external_shift.remove(dep_mom_pos);
2371
2372 let external_shift = shift_rewrite
2373 .dependent_momentum_expr
2374 .iter()
2375 .map(|(index, sign)| (*index, dep_mom_sign * sign))
2376 .collect();
2377
2378 self.external_shift = add_external_shifts(&self.external_shift, &external_shift);
2379 }
2380 }
2381
2382 pub(crate) fn new_from_cut_left<E, V, H>(
2383 graph: &HedgeGraph<E, V, H>,
2384 cut: &CrossSectionCut,
2385 initial_state_cut: Option<&OrientedCut>,
2386 ) -> Self {
2387 let edges = graph
2388 .iter_edges_of(&cut.cut)
2389 .map(|(_, id, _)| id)
2390 .sorted()
2391 .collect();
2392
2393 let external_shift = if let Some(is_cut) = initial_state_cut {
2394 graph
2395 .iter_edges_of(is_cut)
2396 .map(|(_, edge_index, __)| (edge_index, -1))
2397 .sorted_by(|a, b| a.0.cmp(&b.0))
2398 .collect()
2399 } else {
2400 graph
2401 .iter_edges_of(&cut.left)
2402 .filter_map(|(hedge_pair, edge_index, _)| match hedge_pair {
2403 HedgePair::Unpaired { flow, .. } => match flow {
2404 Flow::Sink => Some((edge_index, -1)),
2405 Flow::Source => Some((edge_index, 1)),
2406 },
2407 _ => None,
2408 })
2409 .sorted_by(|a, b| a.0.cmp(&b.0))
2410 .collect()
2411 };
2412
2413 let vertex_set = graph
2414 .iter_nodes_of(&cut.left)
2415 .map(|(node_id, _, _)| VertexSet::from_usize(node_id.into()))
2416 .reduce(|acc, v| acc.join(&v))
2417 .unwrap();
2418
2419 Self {
2420 energies: edges,
2421 external_shift,
2422 vertex_set,
2423 }
2425 }
2426
2427 pub(crate) fn lmb_atom(&self, graph: &Graph, lmb_reps: &[Replacement]) -> Atom {
2428 self.energies
2429 .iter()
2430 .map(|index| {
2431 let mass_symbol = graph.underlying[*index].mass_atom();
2432 let emr_symbols = (0..3)
2433 .map(|i| function!(GS.emr_mom, usize::from(*index), i + 1))
2434 .collect_vec();
2435
2436 (&emr_symbols[0] * &emr_symbols[0]
2437 + &emr_symbols[1] * &emr_symbols[1]
2438 + &emr_symbols[2] * &emr_symbols[2]
2439 + &mass_symbol * &mass_symbol)
2440 .sqrt()
2441 })
2442 .chain(self.external_shift.iter().map(|(index, sign)| {
2443 function!(GS.emr_mom, usize::from(*index), 0) * Atom::num(*sign)
2444 }))
2445 .reduce(|sum, atom| sum + atom)
2446 .unwrap_or_else(Atom::new)
2447 .replace_multiple(lmb_reps)
2448 .replace(parse!("ZERO"))
2449 .with(Atom::new())
2450 .expand() }
2452
2453 pub(crate) fn lmb_atom_simplified(&self, graph: &Graph, lmb_reps: &[Replacement]) -> Atom {
2455 self.energies
2456 .iter()
2457 .map(|index| {
2458 let mass_symbol = graph.underlying[*index].mass_atom();
2459 let emr_symbol = function!(GS.emr_mom, usize::from(*index));
2460
2461 (&emr_symbol * &emr_symbol + &mass_symbol * &mass_symbol).sqrt()
2462 })
2463 .chain(self.external_shift.iter().map(|(index, sign)| {
2464 function!(GS.emr_mom, usize::from(*index), 0) * Atom::num(*sign)
2465 }))
2466 .reduce(|sum, atom| sum + atom)
2467 .unwrap_or_else(Atom::new)
2468 .replace_multiple(lmb_reps)
2469 .replace(parse!("ZERO"))
2470 .with(Atom::new())
2471 .expand() }
2473}
2474
2475define_index! {pub struct GroupEsurfaceId;}
2476
2477pub type EsurfaceCollection = TiVec<EsurfaceID, Esurface>;
2478
2479pub type EsurfaceCache<T> = TiVec<EsurfaceID, T>;
2480
2481pub type ExistingEsurfaces = TiVec<ExistingEsurfaceId, GroupEsurfaceId>;
2483pub type ExistingThresholds = TiVec<ExistingEsurfaceId, EsurfaceID>;
2484
2485pub(crate) fn get_representative<T: Copy>(
2486 esurface_map: &TiVec<GraphGroupPosition, Option<T>>,
2487) -> Result<(GraphGroupPosition, T)> {
2488 for (group_pos, esurface_option) in esurface_map.iter_enumerated() {
2489 if let Some(esurface_id) = esurface_option {
2490 return Ok((group_pos, *esurface_id));
2491 }
2492 }
2493
2494 Err(eyre!(
2495 "No representative esurface found, esurface map corrupted"
2496 ))
2497}
2498
2499#[derive(
2501 Debug,
2502 From,
2503 Into,
2504 Copy,
2505 Clone,
2506 Serialize,
2507 Deserialize,
2508 PartialEq,
2509 Eq,
2510 Hash,
2511 PartialOrd,
2512 Ord,
2513 Encode,
2514 Decode,
2515)]
2516pub struct ExistingEsurfaceId(usize);
2517
2518impl Display for ExistingEsurfaceId {
2519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2520 write!(f, "ExistingEsurfaceID({})", self.0)
2521 }
2522}
2523
2524pub type ExternalShift = Vec<(EdgeIndex, i64)>;
2525
2526pub(crate) fn add_external_shifts(lhs: &ExternalShift, rhs: &ExternalShift) -> ExternalShift {
2528 let mut res = lhs.clone();
2529
2530 for rhs_element in rhs.iter() {
2531 if let Some(lhs_element) = res
2532 .iter_mut()
2533 .find(|lhs_element| rhs_element.0 == lhs_element.0)
2534 {
2535 lhs_element.1 += rhs_element.1;
2536 } else {
2537 res.push(*rhs_element)
2538 }
2539 }
2540
2541 res.retain(|(_index, sign)| *sign != 0);
2542 res.sort_by_key(|(index, _)| *index);
2543 res
2544}
2545
2546define_index!(
2547 pub struct RaisedEsurfaceId;
2548);
2549
2550#[derive(Debug, Clone, Encode, Decode)]
2551#[trait_decode(trait = GammaLoopContext)]
2552pub struct RaisedEsurfaceData {
2553 pub raised_groups: TiVec<RaisedEsurfaceId, RaisedEsurfaceGroup>,
2554 pub pass_two_evaluator: Option<Vec<GenericEvaluator>>,
2555}
2556
2557#[derive(Debug, Clone, Encode, Decode, PartialEq, Hash, Eq, PartialOrd, Ord)]
2558pub struct RaisedEsurfaceGroup {
2559 pub esurface_ids: Vec<EsurfaceID>,
2560 pub max_occurence: usize,
2561}
2562
2563impl RaisedEsurfaceGroupView for RaisedEsurfaceGroup {
2564 fn esurface_ids(&self) -> &[EsurfaceID] {
2565 &self.esurface_ids
2566 }
2567
2568 fn max_occurrence(&self) -> usize {
2569 self.max_occurence
2570 }
2571}
2572
2573impl RaisedEsurfaceDataView for RaisedEsurfaceData {
2574 fn for_each_raised_group(&self, mut f: impl FnMut(&dyn RaisedEsurfaceGroupView)) {
2575 for group in &self.raised_groups {
2576 f(group);
2577 }
2578 }
2579}
2580
2581impl Graph {
2582 pub(crate) fn normalize_esurface_with_raised_edge_groups(
2583 esurface: &Esurface,
2584 raised_edges: &[Vec<EdgeIndex>],
2585 ) -> Esurface {
2586 let mut normalized = esurface.clone();
2587 for energy in normalized.energies.iter_mut() {
2588 if let Some(representative) = raised_edges
2589 .iter()
2590 .find(|group| group.contains(energy))
2591 .and_then(|group| group.first())
2592 {
2593 *energy = *representative;
2594 }
2595 }
2596 normalized.energies.sort();
2597 normalized
2598 }
2599
2600 pub(crate) fn determine_raised_esurfaces_from_expression(
2601 &self,
2602 expr: &CFFExpression<OrientationID>,
2603 ) -> RaisedEsurfaceData {
2604 let raised_edges = self.get_raised_edge_groups();
2605 let normalized_cut_esurfaces = expr
2606 .surfaces
2607 .esurface_cache
2608 .iter()
2609 .map(|esurface| {
2610 Self::normalize_esurface_with_raised_edge_groups(esurface, &raised_edges)
2611 })
2612 .collect::<TiVec<EsurfaceID, _>>();
2613
2614 let mut raised_groups = TiVec::<RaisedEsurfaceId, RaisedEsurfaceGroup>::new();
2615
2616 for (esurface_id, normalized_cut_esurface) in normalized_cut_esurfaces.iter_enumerated() {
2617 let raised_esurface_group_id = raised_groups.iter_enumerated().find_map(
2618 |(raised_esurface_group_id, esurface_group)| {
2619 if esurface_group
2620 .esurface_ids
2621 .iter()
2622 .all(|esurface_id_in_group| {
2623 normalized_cut_esurfaces[*esurface_id_in_group].energies
2624 == normalized_cut_esurface.energies
2625 && normalized_cut_esurfaces[*esurface_id_in_group].external_shift
2626 == normalized_cut_esurface.external_shift
2627 })
2628 {
2629 Some(raised_esurface_group_id)
2630 } else {
2631 None
2632 }
2633 },
2634 );
2635
2636 if let Some(found_group_id) = raised_esurface_group_id {
2637 raised_groups[found_group_id].esurface_ids.push(esurface_id);
2638 } else {
2639 raised_groups.push(RaisedEsurfaceGroup {
2640 esurface_ids: vec![esurface_id],
2641 max_occurence: 0,
2642 });
2643 }
2644 }
2645
2646 let mut result = RaisedEsurfaceData {
2647 raised_groups,
2648 pass_two_evaluator: None,
2649 };
2650
2651 let mut expression_copy = expr.clone();
2652 expression_copy.normalize_wrt_all_raisings(&result);
2653
2654 for cut_group in result.raised_groups.iter_mut() {
2655 let representative_esurface_id = cut_group.esurface_ids[0];
2656
2657 let max_occurence_for_this_id = expression_copy
2658 .orientations
2659 .iter()
2660 .map(|orientation_expression| {
2661 orientation_expression.max_effective_denominator_value_count_on_branch(
2662 &crate::cff::surface::HybridSurfaceID::Esurface(representative_esurface_id),
2663 )
2664 })
2665 .max()
2666 .unwrap_or(0);
2667
2668 cut_group.max_occurence = max_occurence_for_this_id;
2669 }
2670
2671 result
2672 }
2673}
2674
2675#[cfg(test)]
2676mod tests {
2677 use itertools::Itertools;
2678 use linnet::half_edge::HedgeGraph;
2679 use linnet::half_edge::builder::HedgeGraphBuilder;
2680 use linnet::half_edge::involution::{EdgeIndex, Flow, Orientation};
2681 use linnet::half_edge::nodestore::NodeStorageVec;
2682 use linnet::half_edge::subgraph::{SuBitGraph, SubSetLike};
2683 use symbolica::atom::{Atom, AtomCore};
2684 use symbolica::parse;
2685
2686 use crate::cff::VertexSet;
2687 use crate::graph::{Graph, LmbIndex, LoopMomentumBasis, parse::from_dot::IntoGraph};
2688 use crate::initialisation::test_initialise;
2689 use crate::integrands::process::SamplingMapAffine;
2690 use crate::momentum::{
2691 FourMomentum, ThreeMomentum,
2692 sample::{ExternalFourMomenta, LoopMomenta, SubspaceData},
2693 signature::LoopExtSignature,
2694 };
2695 use crate::processes::CrossSectionCut;
2696 use crate::{
2697 cff::{esurface::Esurface, generation::ShiftRewrite},
2698 dot,
2699 utils::{
2700 ArbPrec, DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, ESURFACE_SHIFT_THRESHOLD, F, FloatLike,
2701 QuadFloat,
2702 newton_solver::{
2703 RadialRootDiagnostics, RadialRootIdentity, SafeguardedNewtonError,
2704 safeguarded_newton_iteration_and_derivative,
2705 },
2706 test_utils::dummy_hedge_graph,
2707 },
2708 };
2709 use typed_index_collections::ti_vec;
2710
2711 use super::{EsurfaceExistence, add_external_shifts};
2712
2713 #[test]
2714 fn canonical_ray_embedding_preserves_ordered_occurrences_and_native_coefficients() {
2715 use super::EsurfaceRay;
2716 use crate::utils::SamplingFloat;
2717
2718 fn check<T: FloatLike>(marker: F<T>) {
2719 let one = marker.one();
2720 let velocity = ThreeMomentum::new(marker.clone(), -marker.clone(), one.zero());
2721 let offset = ThreeMomentum::new(
2722 one.from_usize(2),
2723 &marker / one.from_usize(3),
2724 F(T::from_f64_exact_binary(-0.0)),
2725 );
2726 let repeated = (EdgeIndex(4), velocity.clone(), offset.clone(), one.clone());
2727 let ray = EsurfaceRay {
2728 energies: vec![
2729 repeated.clone(),
2730 (EdgeIndex(7), offset, velocity, marker.clone()),
2731 repeated,
2732 ],
2733 shift: -marker.clone(),
2734 };
2735 let canonical = ray.to_arb_exact().unwrap();
2736 assert_eq!(
2737 canonical.energies.iter().map(|entry| entry.0).collect_vec(),
2738 [EdgeIndex(4), EdgeIndex(7), EdgeIndex(4)]
2739 );
2740 assert_eq!(canonical.energies[0], canonical.energies[2]);
2741 assert_eq!(canonical.energies[0].1.px, marker.to_arb_exact().unwrap());
2742 assert_eq!(
2745 canonical.energies[0]
2746 .2
2747 .pz
2748 .0
2749 .mpfr_enclosure(1000)
2750 .0
2751 .is_sign_negative(),
2752 ray.energies[0].2.pz.0.into_f64().is_sign_negative()
2753 );
2754 let roundtrip = canonical.materialize::<T>().unwrap();
2755 assert_eq!(roundtrip.energies, ray.energies);
2756 assert_eq!(roundtrip.shift, ray.shift);
2757 for field in 0..4 {
2758 let mut invalid = ray.clone();
2759 let nonfinite = F(T::from_f64_exact_binary(f64::INFINITY));
2760 match field {
2761 0 => invalid.energies[0].1.px = nonfinite,
2762 1 => invalid.energies[0].2.py = nonfinite,
2763 2 => invalid.energies[0].3 = nonfinite,
2764 _ => invalid.shift = nonfinite,
2765 }
2766 assert!(invalid.to_arb_exact().is_err());
2767 }
2768 }
2769 let one = F::<QuadFloat>::default().one();
2770 check(one + one.from_i64(2).powi(-80));
2771 let one = F::<SamplingFloat>::default().one();
2772 check(&one + one.from_i64(2).powi(-180) + one.from_i64(2).powi(-240));
2773 }
2774
2775 #[test]
2776 fn joint_sampling_matches_distinct_reversed_edges_and_unequal_masses() {
2777 test_initialise().unwrap();
2778 let graph: Graph = dot!(digraph joint_serial_energy {
2781 ext_in [style=invis]
2782 ext_out [style=invis]
2783 node [num=1]
2784 edge [num=1 mass=1]
2785 ext_in -> A:0 [id=0 mass=0]
2786 C:1 -> ext_out [id=1 mass=0]
2787 A -> B [id=2 mass=2]
2788 B -> C [id=3]
2789 C -> D [id=4 lmb_id=0]
2790 D -> A [id=5 mass=3]
2791 B -> E [id=6 lmb_id=1]
2792 D -> E [id=7]
2793 })
2794 .unwrap();
2795 let lmbs = ti_vec![graph.loop_momentum_basis.clone()];
2796 let id = LmbIndex::from(0);
2797 let lmb = &lmbs[id];
2798 let subspace = SubspaceData::new_from_parent_basis_edges(
2799 &[EdgeIndex(6)],
2800 &graph.full_filter(),
2801 id,
2802 &graph,
2803 &lmbs,
2804 )
2805 .unwrap();
2806 let active = subspace.iter_lmb_indices().next().unwrap();
2807 let prior = lmb
2808 .loop_edges
2809 .iter_enumerated()
2810 .find_map(|(index, edge)| (*edge == EdgeIndex(4)).then_some(index))
2811 .unwrap();
2812 let left = Esurface {
2813 energies: vec![EdgeIndex(2), EdgeIndex(4), EdgeIndex(6)],
2814 external_shift: vec![(EdgeIndex(0), -1)],
2815 vertex_set: VertexSet::dummy(),
2816 };
2817 let right = Esurface {
2818 energies: vec![EdgeIndex(3), EdgeIndex(5), EdgeIndex(7)],
2819 ..left.clone()
2820 };
2821 let masses = graph
2822 .underlying
2823 .new_edgevec_from_iter([0., 0., 2., 1., 1., 3., 1., 1.].map(F))
2824 .unwrap();
2825 let externals = ExternalFourMomenta::from_iter(
2826 [FourMomentum::from_args(F(26_f64.sqrt()), F(0.), F(0.), F(1.)); 2],
2827 );
2828 let (prepare, common) = left
2829 .sampling_joint_geometry_in_subspace(
2830 &right,
2831 &subspace,
2832 &lmbs,
2833 &graph,
2834 &masses,
2835 &externals,
2836 &[prior],
2837 )
2838 .unwrap();
2839 let geometry = prepare(&[1., 0., -0.5]).unwrap();
2840 assert_eq!(geometry.masses, [1., 2., 3.]);
2841 assert_eq!(geometry.shifts, [[1., 0., 0.5], [1., 0., -0.5]]);
2842 assert_ne!(
2843 lmb.edge_signatures[EdgeIndex(6)],
2844 lmb.edge_signatures[EdgeIndex(7)]
2845 );
2846 assert!(lmb.edges_are_raised(EdgeIndex(6), EdgeIndex(7)));
2847 assert_eq!(common, lmb.edge_signatures[EdgeIndex(6)]);
2848 let mut loops = LoopMomenta::from_iter([ThreeMomentum::new(F(0.), F(0.), F(0.)); 2]);
2849 loops[prior] = ThreeMomentum::new(F(1.), F(0.), F(-0.5));
2850 for components in [[0.5, -1., 1.], [-0.7, 0.3, -0.4]] {
2851 let x = ThreeMomentum::new(F(components[0]), F(components[1]), F(components[2]));
2852 loops[active] = x;
2853 let common_energy = (x.norm_squared() + F(1.)).sqrt();
2854 for (i, surface) in [&left, &right].into_iter().enumerate() {
2855 let [a, b, c] = geometry.shifts[i];
2856 let partner = x + ThreeMomentum::new(F(a), F(b), F(c));
2857 let expected = common_energy
2858 + (partner.norm_squared() + F(geometry.masses[i + 1]).square()).sqrt()
2859 - F(geometry.energy_sums[i]);
2860 assert!(
2861 (surface.compute_from_momenta(lmb, &masses, &loops, &externals) - expected)
2862 .abs()
2863 < F(1e-12)
2864 );
2865 }
2866 }
2867 let mut repeated = left.clone();
2869 repeated.energies.push(EdgeIndex(4));
2870 let (prepare_repeated, _) = repeated
2871 .sampling_joint_geometry_in_subspace(
2872 &right,
2873 &subspace,
2874 &lmbs,
2875 &graph,
2876 &masses,
2877 &externals,
2878 &[prior],
2879 )
2880 .unwrap();
2881 assert!(
2882 (prepare_repeated(&[1., 0., -0.5]).unwrap().energy_sums[0]
2883 - (geometry.energy_sums[0] - 1.5))
2884 .abs()
2885 < 1e-12
2886 );
2887
2888 let mut inconsistent_masses = masses.clone();
2889 inconsistent_masses[EdgeIndex(7)] = F(2.);
2890 assert!(
2891 left.sampling_joint_geometry_in_subspace(
2892 &right,
2893 &subspace,
2894 &lmbs,
2895 &graph,
2896 &inconsistent_masses,
2897 &externals,
2898 &[prior],
2899 )
2900 .err()
2901 .unwrap()
2902 .to_string()
2903 .contains("inconsistent native values")
2904 );
2905 let mut different_mass_graph = graph.clone();
2908 different_mass_graph.underlying[EdgeIndex(7)].particle =
2909 crate::graph::edge::PossibleParticle::JustMass { expr: parse!("2") };
2910 assert!(
2911 left.sampling_joint_geometry_in_subspace(
2912 &right,
2913 &subspace,
2914 &lmbs,
2915 &different_mass_graph,
2916 &masses,
2917 &externals,
2918 &[prior],
2919 )
2920 .err()
2921 .unwrap()
2922 .to_string()
2923 .contains("found 0 candidates")
2924 );
2925 }
2926
2927 #[test]
2928 fn joint_sampling_matches_exact_external_cm_relation_only() {
2929 test_initialise().unwrap();
2930 let graph: Graph = dot!(digraph joint_external_energy {
2933 ext_in [style=invis]
2934 ext_out [style=invis]
2935 beam_a [style=invis]
2936 beam_b [style=invis]
2937 node [num=1]
2938 edge [num=1 mass=1]
2939 ext_in -> A:0 [id=0 mass=0]
2940 C:1 -> ext_out [id=1 mass=0]
2941 A -> B [id=2 mass=2]
2942 B -> C [id=3]
2943 C -> D [id=4 lmb_id=0]
2944 D -> A [id=5 mass=3]
2945 B -> E [id=6 lmb_id=1]
2946 D -> E [id=7]
2947 beam_a -> E [id=8 mass=0]
2948 beam_b -> E [id=9 mass=0]
2949 })
2950 .unwrap();
2951 fn check<T: FloatLike>(graph: &Graph) {
2952 let one = F::<T>::default().one();
2953 let zero = one.zero();
2954 let lmbs = ti_vec![graph.loop_momentum_basis.clone()];
2955 let id = LmbIndex::from(0);
2956 let lmb = &lmbs[id];
2957 let subspace = SubspaceData::new_from_parent_basis_edges(
2958 &[EdgeIndex(6)],
2959 &graph.full_filter(),
2960 id,
2961 graph,
2962 &lmbs,
2963 )
2964 .unwrap();
2965 let active = subspace.iter_lmb_indices().next().unwrap();
2966 let prior = lmb
2967 .loop_edges
2968 .iter_enumerated()
2969 .find_map(|(index, edge)| (*edge == EdgeIndex(4)).then_some(index))
2970 .unwrap();
2971 let left = Esurface {
2972 energies: vec![EdgeIndex(2), EdgeIndex(4), EdgeIndex(6)],
2973 external_shift: vec![(EdgeIndex(0), -1)],
2974 vertex_set: VertexSet::dummy(),
2975 };
2976 let right = Esurface {
2977 energies: vec![EdgeIndex(3), EdgeIndex(5), EdgeIndex(7)],
2978 ..left.clone()
2979 };
2980 let masses = graph
2981 .underlying
2982 .new_edgevec_from_iter(
2983 [0, 0, 2, 1, 1, 3, 1, 1, 0, 0].map(|mass| one.from_usize(mass)),
2984 )
2985 .unwrap();
2986 assert!(
2987 !lmb.edge_signatures[EdgeIndex(6)]
2988 .equality_up_to_sign(&lmb.edge_signatures[EdgeIndex(7)])
2989 );
2990 assert!(!lmb.edges_are_raised(EdgeIndex(6), EdgeIndex(7)));
2991 for boosted in [false, true] {
2992 let externals = ExternalFourMomenta::from_iter(lmb.ext_edges.iter().map(|edge| {
2993 let (energy, z) = match edge.0 {
2994 0 => (20, 0),
2995 1 => (26, 0),
2996 8 => (3, 2),
2997 9 => (3, -2),
2998 _ => unreachable!(),
2999 };
3000 let (energy, z) = (one.from_i64(energy), one.from_i64(z));
3001 let (energy, z) = if boosted {
3003 (
3004 (one.from_i64(5) * &energy + one.from_i64(3) * &z) / one.from_i64(4),
3005 (one.from_i64(3) * energy + one.from_i64(5) * z) / one.from_i64(4),
3006 )
3007 } else {
3008 (energy, z)
3009 };
3010 FourMomentum::from_args(energy, zero.clone(), zero.clone(), z)
3011 }));
3012 let matched = left.sampling_joint_geometry_in_subspace(
3013 &right,
3014 &subspace,
3015 &lmbs,
3016 graph,
3017 &masses,
3018 &externals,
3019 &[prior],
3020 );
3021 if boosted {
3022 assert!(
3023 matched
3024 .err()
3025 .unwrap()
3026 .to_string()
3027 .contains("found 0 candidates")
3028 );
3029 continue;
3030 }
3031 let (prepare, common) = matched.unwrap();
3032 let prior_values = [one.clone(), zero.clone(), -(&one / one.from_i64(2))];
3033 let geometry = prepare(&prior_values.clone().map(|x| x.0)).unwrap();
3034 assert_eq!(
3035 geometry.masses,
3036 [one.0.clone(), one.from_i64(2).0, one.from_i64(3).0]
3037 );
3038 let mut loops = LoopMomenta::from_iter(
3039 (0..2).map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
3040 );
3041 loops[prior] = ThreeMomentum::new(
3042 prior_values[0].clone(),
3043 prior_values[1].clone(),
3044 prior_values[2].clone(),
3045 );
3046 let spatial: crate::momentum::sample::ExternalThreeMomenta<F<T>> =
3047 externals.iter().map(|p| p.spatial.clone()).collect();
3048 for values in [[1, -2, 3], [-3, 1, -1]] {
3049 let values = values.map(|value| one.from_i64(value) / one.from_i64(4));
3050 loops[active] =
3051 ThreeMomentum::new(values[0].clone(), values[1].clone(), values[2].clone());
3052 let x: ThreeMomentum<F<T>> = common.compute_momentum(&loops, &spatial);
3053 let e0 = (x.norm_squared() + F(geometry.masses[0].clone()).square()).sqrt();
3054 for (index, surface) in [&left, &right].into_iter().enumerate() {
3055 let shift = geometry.shifts[index].each_ref().map(|x| F(x.clone()));
3056 let partner = &x
3057 + &ThreeMomentum::new(
3058 shift[0].clone(),
3059 shift[1].clone(),
3060 shift[2].clone(),
3061 );
3062 let expected = &e0
3063 + (partner.norm_squared()
3064 + F(geometry.masses[index + 1].clone()).square())
3065 .sqrt()
3066 - F(geometry.energy_sums[index].clone());
3067 let original =
3068 surface.compute_from_momenta(lmb, &masses, &loops, &externals);
3069 assert!(
3070 (original - expected).abs() < one.epsilon().sqrt() * one.from_i64(100)
3071 );
3072 }
3073 }
3074 let fixed: ThreeMomentum<F<T>> =
3077 lmb.edge_signatures[EdgeIndex(4)].compute_momentum(&loops, &spatial);
3078 let fixed_energy = (fixed.norm_squared() + masses[EdgeIndex(4)].square()).sqrt();
3079 assert!(
3080 (F(geometry.energy_sums[0].clone())
3081 + fixed_energy
3082 + left.compute_shift_part_from_momenta(&externals, lmb))
3083 .abs()
3084 < one.epsilon().sqrt() * one.from_i64(100)
3085 );
3086 }
3087 }
3088 check::<f64>(&graph);
3089 check::<QuadFloat>(&graph);
3090 check::<ArbPrec>(&graph);
3091 }
3092
3093 #[test]
3094 fn routed_enclosure_preserves_original_terms_and_native_rescale() {
3095 use super::{EsurfaceRay, SamplingEvaluationError};
3096 use crate::momentum::sample::LoopIndex;
3097 use rug::Float;
3098
3099 fn check<T: FloatLike>() {
3100 let one = F::<T>::default().one();
3101 let zero = one.zero();
3102 let graph = dummy_hedge_graph(11);
3103 let mut routes = (0..5)
3104 .map(|i| {
3105 let mut internal = vec![0; 5];
3106 internal[i] = 1;
3107 LoopExtSignature::from((internal, vec![0; 2]))
3108 })
3109 .collect::<Vec<_>>();
3110 routes.extend([
3111 LoopExtSignature::from((vec![1, 1, 1, -1, -1], vec![1, 0])),
3112 LoopExtSignature::from((vec![0; 5], vec![0, 1])),
3113 LoopExtSignature::from((vec![0; 5], vec![0, 0])),
3114 LoopExtSignature::from((vec![0; 5], vec![1, -1])),
3115 LoopExtSignature::from((vec![0; 5], vec![1, 0])),
3116 LoopExtSignature::from((vec![0; 5], vec![0, 1])),
3117 ]);
3118 let lmb = LoopMomentumBasis {
3119 tree: SuBitGraph::empty(0),
3120 loop_edges: (0..5).map(EdgeIndex).collect(),
3121 ext_edges: vec![EdgeIndex(9), EdgeIndex(10)].into(),
3122 edge_signatures: graph.new_edgevec_from_iter(routes).unwrap(),
3123 };
3124 let big = one.clone() / one.epsilon().square();
3125 let middle = big.sqrt();
3126 let vector = |x| ThreeMomentum::new(x, zero.clone(), zero.clone());
3127 let loops = LoopMomenta::from_iter(
3130 [big.clone(), middle.clone(), one.clone(), middle, big].map(&vector),
3131 );
3132 let externals = ExternalFourMomenta::from_iter([
3133 FourMomentum::from_args(
3134 one.from_i64(7),
3135 one.from_i64(3),
3136 zero.clone(),
3137 zero.clone(),
3138 ),
3139 FourMomentum::from_args(
3140 one.from_i64(11),
3141 one.from_i64(4),
3142 zero.clone(),
3143 zero.clone(),
3144 ),
3145 ]);
3146 let mut masses = graph
3147 .new_edgevec_from_iter(vec![one.from_i64(3); 11])
3148 .unwrap();
3149 masses[EdgeIndex(7)] = zero.clone();
3150 let surface = Esurface {
3151 energies: vec![EdgeIndex(5), EdgeIndex(5), EdgeIndex(6), EdgeIndex(7)],
3152 external_shift: vec![(EdgeIndex(8), 3), (EdgeIndex(6), -1)],
3153 vertex_set: VertexSet::dummy(),
3154 };
3155 let bounds = surface
3158 .evaluate_routed_enclosed(&one, &loops, &externals, &masses, &lmb)
3159 .unwrap();
3160 assert_eq!(
3161 bounds,
3162 [Float::with_val(2048, -8), Float::with_val(2048, -8)]
3163 );
3164 let center = LoopMomenta::from_iter((0..5).map(|_| vector(zero.clone())));
3165 let represented = surface.routed_ray(&loops, ¢er, &externals, &masses, &lmb);
3166 assert_eq!(represented.energies[0].1.px, zero);
3167 let old_value = represented.evaluate(&one).0;
3168 assert!(
3169 old_value < one.from_i64(-9),
3170 "rounded routing must fail the exact -8 oracle"
3171 );
3172
3173 let tau = &one + one.epsilon();
3176 let mut loops = center;
3177 loops[LoopIndex(0)] = vector(tau.clone());
3178 let scalar = Esurface {
3179 energies: vec![EdgeIndex(0)],
3180 external_shift: vec![],
3181 vertex_set: VertexSet::dummy(),
3182 };
3183 masses[EdgeIndex(0)] = zero.clone();
3184 let mathematical = scalar
3185 .evaluate_routed_enclosed(&tau, &loops, &externals, &masses, &lmb)
3186 .unwrap();
3187 let completed = loops.rescale(&tau, None);
3188 let actual = scalar
3189 .evaluate_routed_enclosed(&one, &completed, &externals, &masses, &lmb)
3190 .unwrap();
3191 assert!(
3192 mathematical[0] > actual[1],
3193 "the omitted epsilon^2 term must be resolved"
3194 );
3195 let actual_native = EsurfaceRay::<T>::enclosed_native(&completed[LoopIndex(0)].px);
3196 assert!(actual[0] <= actual_native[0] && actual[1] >= actual_native[1]);
3197
3198 let missing = LoopMomenta::from_iter(loops.iter().take(4).cloned());
3199 assert!(
3200 scalar
3201 .evaluate_routed_enclosed(&one, &missing, &externals, &masses, &lmb)
3202 .is_err()
3203 );
3204 loops[LoopIndex(0)].px = zero.clone() / zero.clone();
3205 assert!(matches!(
3206 scalar
3207 .evaluate_routed_enclosed(&one, &loops, &externals, &masses, &lmb)
3208 .unwrap_err()
3209 .downcast_ref::<SamplingEvaluationError>(),
3210 Some(SamplingEvaluationError::Unrepresentable { .. })
3211 ));
3212 }
3213 check::<f64>();
3214 check::<QuadFloat>();
3215 check::<ArbPrec>();
3216 }
3217
3218 #[test]
3219 fn hosted_normal_alignment_enforces_directed_half_budgets() {
3220 use super::{EsurfaceRay, SamplingEvaluationError};
3221 use rug::Float;
3222 let point = |value: i32| {
3223 let x = Float::with_val(2048, value);
3224 [x.clone(), x]
3225 };
3226 let canonical = [point(3), point(4)];
3227 let half_budget: Float = Float::with_val(2048, 5) / 16;
3228 let shifted: Float = Float::with_val(2048, 3) + &half_budget;
3229 let native = [[shifted.clone(), shifted], point(4)];
3230 let host = [-half_budget.clone(), -half_budget.clone()];
3231 EsurfaceRay::<f64>::verify_normal_alignment(
3233 canonical.clone(),
3234 native.clone(),
3235 host.clone(),
3236 0.125,
3237 )
3238 .unwrap();
3239 let beyond: Float = Float::with_val(2048, 1) >> 1000;
3240 let mut bad_normal = native;
3241 bad_normal[0][1] += &beyond;
3242 let mut bad_host = host;
3243 bad_host[0] -= beyond;
3244 for (native, host) in [(bad_normal, point(0)), (canonical.clone(), bad_host)] {
3245 assert!(matches!(
3246 EsurfaceRay::<f64>::verify_normal_alignment(canonical.clone(), native, host, 0.125)
3247 .unwrap_err()
3248 .downcast_ref::<SamplingEvaluationError>(),
3249 Some(SamplingEvaluationError::UncertainGeometry { .. })
3250 ));
3251 }
3252 for canonical in [
3253 [point(0), point(0)],
3254 [
3255 [Float::with_val(2048, -1), Float::with_val(2048, 1)],
3256 point(0),
3257 ],
3258 ] {
3259 assert!(matches!(
3260 EsurfaceRay::<f64>::verify_normal_alignment(
3261 canonical.clone(),
3262 canonical,
3263 point(0),
3264 0.125
3265 )
3266 .unwrap_err()
3267 .downcast_ref::<SamplingEvaluationError>(),
3268 Some(SamplingEvaluationError::UncertainGeometry { .. })
3269 ));
3270 }
3271 assert!(
3272 EsurfaceRay::<f64>::verify_normal_alignment(
3273 canonical.clone(),
3274 canonical.clone(),
3275 point(0),
3276 0.0
3277 )
3278 .is_err()
3279 );
3280 assert!(matches!(
3281 EsurfaceRay::<f64>::verify_normal_alignment(
3282 canonical.clone(),
3283 canonical,
3284 std::array::from_fn(|_| Float::with_val(2048, rug::float::Special::Infinity)),
3285 0.125
3286 )
3287 .unwrap_err()
3288 .downcast_ref::<SamplingEvaluationError>(),
3289 Some(SamplingEvaluationError::Unrepresentable { .. })
3290 ));
3291 }
3292
3293 #[test]
3294 fn retained_lu_candidate_requires_directed_native_agreement() {
3295 use super::EsurfaceRay;
3296 use crate::{
3297 integrands::process::sampling::maps::SamplingEvaluationError,
3298 momentum::{Rotatable, Rotation, RotationMethod},
3299 };
3300 fn check<T: FloatLike>() {
3301 let one = F::<T>::default().one();
3302 let f = |n| one.from_usize(n);
3303 let vector = |x| ThreeMomentum::new(x, one.zero(), one.zero());
3304 let ray = EsurfaceRay {
3307 energies: vec![(EdgeIndex(1), vector(f(3)), vector(one.clone()), f(3))],
3308 shift: -f(6),
3309 };
3310 let solution = ray
3311 .solve_lu_cut(
3312 &f(6),
3313 &mut RadialRootDiagnostics::default(),
3314 &RadialRootIdentity::new("directed native host fixture".into()),
3315 )
3316 .unwrap();
3317 let expected = (f(27).sqrt() - &one) / f(3);
3318 assert!((&solution.solution - expected).abs() < one.epsilon() * f(100));
3319 let budget = &one / f(1_000_000);
3320 ray.verify_lu_candidate(&ray, &solution, 6, &budget)
3321 .unwrap();
3322 let rotation = Rotation::new(RotationMethod::EulerAngles(0.31, -0.17, 0.23));
3323 ray.rotate(&rotation)
3324 .verify_lu_candidate(&ray, &solution, 6, &budget)
3325 .unwrap();
3326
3327 let tiny = &one / f(10).powi(200);
3332 let mut shallow = ray.clone();
3333 for (_, v, b, mass) in &mut shallow.energies {
3334 *v = &*v * &tiny;
3335 *b = &*b * &tiny;
3336 *mass *= &tiny;
3337 }
3338 shallow.shift *= &tiny;
3339 let mut shallow_solution = solution.clone();
3340 shallow_solution.derivative_at_solution *= &tiny;
3341 shallow_solution.error_of_function *= &tiny;
3342 shallow
3343 .verify_lu_candidate(&shallow, &shallow_solution, 6, &budget)
3344 .unwrap();
3345 let mut tiny_energy = ray.clone();
3346 tiny_energy
3347 .energies
3348 .push((EdgeIndex(2), vector(one.zero()), vector(one.zero()), tiny));
3349 tiny_energy
3350 .verify_lu_candidate(&tiny_energy, &solution, 6, &budget)
3351 .unwrap();
3352 tiny_energy.energies[1].3 = one.zero();
3353 let error = tiny_energy
3354 .verify_lu_candidate(&tiny_energy, &solution, 6, &budget)
3355 .unwrap_err();
3356 assert!(
3357 matches!(
3358 error.downcast_ref::<SamplingEvaluationError>(),
3359 Some(SamplingEvaluationError::UncertainGeometry { .. })
3360 ),
3361 "{error:?}"
3362 );
3363 assert!(
3364 error
3365 .to_string()
3366 .contains("energy lower bound touches zero"),
3367 "{error:?}"
3368 );
3369
3370 let mut wrong = ray.clone();
3371 wrong.shift -= &one / f(100);
3372 let error = ray
3373 .verify_lu_candidate(&wrong, &solution, 6, &budget)
3374 .unwrap_err();
3375 assert!(
3376 matches!(
3377 error.downcast_ref::<SamplingEvaluationError>(),
3378 Some(SamplingEvaluationError::UncertainGeometry { .. })
3379 ),
3380 "{error:?}"
3381 );
3382 let mut wrong_derivative = solution.clone();
3383 wrong_derivative.derivative_at_solution *= f(11) / f(10);
3384 assert!(
3385 ray.verify_lu_candidate(&ray, &wrong_derivative, 6, &budget)
3386 .is_err()
3387 );
3388 wrong = ray.clone();
3389 wrong.energies[0].0 = EdgeIndex(2);
3390 assert!(
3391 ray.verify_lu_candidate(&wrong, &solution, 6, &budget)
3392 .unwrap_err()
3393 .to_string()
3394 .contains("ordered energy occurrences")
3395 );
3396 wrong = ray.clone();
3397 wrong.energies[0].3 = one.zero() / one.zero();
3398 assert!(matches!(
3399 ray.verify_lu_candidate(&wrong, &solution, 6, &budget)
3400 .unwrap_err()
3401 .downcast_ref::<SamplingEvaluationError>(),
3402 Some(SamplingEvaluationError::Unrepresentable { .. })
3403 ));
3404
3405 let tight = &one / f(10).powi(25);
3409 let tight_result = ray.verify_lu_candidate(&ray, &solution, 6, &tight);
3410 let large = f(10).powi(20);
3411 let cancellation = (&large + &one) - &large;
3412 let mut completed = ray.clone();
3413 completed.energies[0].1.px += &cancellation - &one;
3414 let cancellation_result = ray.verify_lu_candidate(&completed, &solution, 6, &budget);
3415 if T::sampling_precision() == crate::utils::SamplingPrecision::Double {
3416 assert_eq!(cancellation, one.zero());
3417 for result in [tight_result, cancellation_result] {
3418 assert!(matches!(
3419 result
3420 .unwrap_err()
3421 .downcast_ref::<SamplingEvaluationError>(),
3422 Some(SamplingEvaluationError::UncertainGeometry { .. })
3423 ));
3424 }
3425 } else {
3426 assert_eq!(cancellation, one);
3427 tight_result.unwrap();
3428 cancellation_result.unwrap();
3429 }
3430 }
3431 check::<f64>();
3432 check::<QuadFloat>();
3433 check::<ArbPrec>();
3434 }
3435
3436 #[test]
3437 fn radial_guesses_and_lu_roots_handle_constant_massive_energies() {
3438 test_initialise().unwrap();
3439 let graph: Graph = dot!(digraph massive_energy_at_rest {
3443 ext [style=invis]
3444 node [num=1]
3445 edge [num=1 mass=0]
3446 ext -> a:0 [id=0]
3447 a -> b [id=1 lmb_id=0]
3448 a -> b [id=2 lmb_id=1]
3449 a -> b [id=3]
3450 b:1 -> ext [id=4]
3451 })
3452 .unwrap();
3453 let lmbs = ti_vec![graph.loop_momentum_basis.clone()];
3454 let lmb = &lmbs[LmbIndex::from(0)];
3455 let subspace = SubspaceData::new_from_parent_basis_edges(
3456 &[EdgeIndex(1), EdgeIndex(2)],
3457 &graph.underlying.full_filter(),
3458 LmbIndex::from(0),
3459 &graph,
3460 &lmbs,
3461 )
3462 .unwrap();
3463 let surface = Esurface {
3464 energies: vec![EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)],
3465 external_shift: vec![(EdgeIndex(0), -1)],
3466 vertex_set: VertexSet::dummy(),
3467 };
3468 let masses = graph
3469 .underlying
3470 .new_edgevec_from_iter([F(0.0), F(173.0), F(173.0), F(125.0), F(0.0)])
3471 .unwrap();
3472 let externals = ExternalFourMomenta::from_iter(
3475 [FourMomentum::from_args(F(1000.0), F(0.0), F(0.0), F(0.0)); 2],
3476 );
3477 let center = LoopMomenta::from_iter([ThreeMomentum::new(F(0.0), F(0.0), F(0.0)); 2]);
3478
3479 for momentum in [0.0, 100.0] {
3480 let loops = LoopMomenta::from_iter([
3481 ThreeMomentum::new(F(momentum), F(0.0), F(0.0)),
3482 ThreeMomentum::new(F(-momentum), F(0.0), F(0.0)),
3483 ]);
3484 let guesses = [
3485 surface.get_radius_guess(&loops, &externals, lmb),
3486 surface.get_radius_guess_subspace(
3487 &loops, &externals, &subspace, &lmbs, &graph, &masses,
3488 ),
3489 ];
3490 for (positive, negative) in guesses {
3491 assert!(positive.0.is_finite());
3492 assert_eq!(negative, -positive);
3493 let result = safeguarded_newton_iteration_and_derivative(
3494 &F(0.0),
3495 &positive,
3496 |radius| {
3497 surface.compute_self_and_r_derivative(
3498 radius, &loops, ¢er, &externals, &masses, lmb,
3499 )
3500 },
3501 &F(1.0),
3502 2000,
3503 64,
3504 &F(1000.0),
3505 );
3506 if momentum == 0.0 {
3507 assert_eq!(positive, F(0.0));
3508 assert!(matches!(
3511 result,
3512 Err(SafeguardedNewtonError::InvalidOutside { .. })
3513 ));
3514 } else {
3515 let result = result.unwrap();
3516 let top_energy = (1000.0_f64 - 125.0) / 2.0;
3517 let expected = (top_energy.powi(2) - 173.0_f64.powi(2)).sqrt() / momentum;
3518 assert!((result.solution.0 - expected).abs() < 1.0e-14);
3519 assert!(result.derivative_at_solution.0 > 0.0);
3520 assert!(result.error_of_function.0.abs() < 1.0e-12);
3521 }
3522 }
3523 }
3524
3525 fn check_native<T: FloatLike>(graph: &Graph, surface: &Esurface) {
3526 let one = F::<T>::default().one();
3527 let zero = one.zero();
3528 let f = |value| one.from_i64(value);
3529 let lmb = &graph.loop_momentum_basis;
3530 let mut masses = graph
3531 .underlying
3532 .new_edgevec_from_iter([0, 173, 173, 125, 0].map(f))
3533 .unwrap();
3534 let e_cm = f(1000);
3535 let externals = ExternalFourMomenta::from_iter((0..2).map(|_| {
3536 FourMomentum::from_args(e_cm.clone(), zero.clone(), zero.clone(), zero.clone())
3537 }));
3538 let zero_vector = ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone());
3539 let center = LoopMomenta::from_iter([zero_vector.clone(), zero_vector.clone()]);
3540 let perturbation = &one / f(10).powi(25);
3541 let p = f(100) + &perturbation;
3542 let loops = LoopMomenta::from_iter([
3543 ThreeMomentum::new(p.clone(), zero.clone(), zero.clone()),
3544 ThreeMomentum::new(-&p, zero.clone(), zero.clone()),
3545 ]);
3546 let identity = RadialRootIdentity::new("native shared LU host".into());
3547 let mut diagnostics = RadialRootDiagnostics::default();
3548 let mut prior_policy = diagnostics.clone();
3549 let (ray, result) = surface
3550 .solve_lu_cut(
3551 &loops,
3552 &externals,
3553 &masses,
3554 lmb,
3555 &e_cm,
3556 &mut diagnostics,
3557 &identity,
3558 )
3559 .unwrap();
3560 let guess = surface.get_radius_guess(&loops, &externals, lmb).0;
3563 assert_eq!(
3564 Esurface::radius_guess_from_terms(
3565 &ray.shift,
3566 ray.energies
3567 .iter()
3568 .map(|(_, v, b, _)| (v.norm_squared(), v.clone() * b)),
3569 ),
3570 guess
3571 );
3572 let prior = prior_policy
3573 .solve(
3574 &identity,
3575 &zero,
3576 &guess,
3577 |t| {
3578 surface.compute_self_and_r_derivative(
3579 t, &loops, ¢er, &externals, &masses, lmb,
3580 )
3581 },
3582 &one,
3583 2000,
3584 64,
3585 &e_cm,
3586 )
3587 .unwrap();
3588 assert_eq!(result.solution, prior.solution);
3589 assert_eq!(result.derivative_at_solution, prior.derivative_at_solution);
3590 assert_eq!(result.error_of_function, prior.error_of_function);
3591 assert_eq!(result.num_iterations_used, prior.num_iterations_used);
3592 let top_energy = (&e_cm - f(125)) / f(2);
3593 let radial_momentum = (top_energy.square() - f(173).square()).sqrt();
3594 assert!((&result.solution - &radial_momentum / &p).abs() < one.epsilon() * f(128));
3595 if perturbation > one.epsilon() * f(1000) {
3596 assert!(
3597 (&result.solution - radial_momentum / f(100)).abs() > &one / f(10).powi(28)
3598 );
3599 }
3600
3601 use crate::utils::hyperdual_utils::{
3604 extract_t_derivatives, new_constant, simple_n_deriv_shape,
3605 };
3606 use symbolica::domains::dual::HyperDual;
3607 let mut jet_surface = surface.clone();
3608 jet_surface.energies = vec![EdgeIndex(1), EdgeIndex(1), EdgeIndex(3)];
3609 let mut jet_masses = masses.clone();
3610 jet_masses[EdgeIndex(1)] = f(3);
3611 jet_masses[EdgeIndex(3)] = f(4);
3612 let jet_velocity = LoopMomenta::from_iter([
3613 ThreeMomentum::new(f(3), zero.clone(), zero.clone()),
3614 ThreeMomentum::new(-f(3), zero.clone(), zero.clone()),
3615 ]);
3616 let jet_center = LoopMomenta::from_iter([
3617 ThreeMomentum::new(one.clone(), zero.clone(), zero.clone()),
3618 ThreeMomentum::new(-&one, zero.clone(), zero.clone()),
3619 ]);
3620 let jet_externals =
3621 ExternalFourMomenta::from_iter((0..2).map(|_| {
3622 FourMomentum::from_args(e_cm.clone(), zero.clone(), f(3), zero.clone())
3623 }));
3624 let jet_ray = jet_surface.routed_ray(
3625 &jet_velocity,
3626 &jet_center,
3627 &jet_externals,
3628 &jet_masses,
3629 lmb,
3630 );
3631 assert_eq!(
3632 jet_ray.energies.iter().map(|term| term.0).collect_vec(),
3633 jet_surface.energies
3634 );
3635 assert_eq!(jet_ray.energies[2].1.norm_squared(), zero);
3636 assert_eq!(jet_ray.energies[2].2.norm_squared(), f(9));
3637 let old_seed = jet_surface
3640 .get_radius_guess(&jet_velocity, &jet_externals, lmb)
3641 .0;
3642 assert_eq!(old_seed, &e_cm / f(6));
3643 let ray_seed = Esurface::radius_guess_from_terms(
3644 &jet_ray.shift,
3645 jet_ray
3646 .energies
3647 .iter()
3648 .map(|(_, v, b, _)| (v.norm_squared(), v.clone() * b)),
3649 );
3650 assert!((ray_seed - old_seed - f(2) / f(3)).abs() < one.epsilon() * f(1024));
3651 let expected = [
3652 f(15) - &e_cm,
3653 f(24) / f(5),
3654 f(162) / f(125),
3655 -f(5832) / f(3125),
3656 ];
3657 let scalar = jet_ray.evaluate(&one);
3658 assert_eq!(scalar.0, expected[0]);
3659 assert!((&scalar.1 - &expected[1]).abs() < one.epsilon() * f(128));
3660 for order in 1..=3 {
3661 let t =
3662 HyperDual::<F<T>>::new(simple_n_deriv_shape(order)).variable(0, one.clone());
3663 let actual = extract_t_derivatives(jet_ray.evaluate_dual(&t));
3664 let dual_loops = LoopMomenta::from_iter(
3667 jet_velocity.iter().zip(jet_center.iter()).map(|(v, b)| {
3668 v.map_ref(&|x| new_constant(&t, x) * &t)
3669 + b.map_ref(&|x| new_constant(&t, x))
3670 }),
3671 );
3672 let dual_externals = jet_externals
3673 .iter()
3674 .map(|p| p.map_ref(&|x| new_constant(&t, x)))
3675 .collect();
3676 let original = extract_t_derivatives(jet_surface.compute_from_dual_momenta(
3677 lmb,
3678 &jet_masses,
3679 &dual_loops,
3680 &dual_externals,
3681 ));
3682 for ((actual, original), expected) in actual.iter().zip(&original).zip(&expected) {
3683 let tolerance = one.epsilon() * f(2048) * (one.clone() + expected.abs());
3684 assert!((actual - expected).abs() < tolerance);
3685 assert!((actual - original).abs() < tolerance);
3686 }
3687 }
3688
3689 let mut cancellation = surface.clone();
3694 cancellation.energies = vec![EdgeIndex(3)];
3695 cancellation.external_shift.clear();
3696 let large = f(10).powi(16);
3697 let velocity = LoopMomenta::from_iter([
3698 ThreeMomentum::new(large.clone(), zero.clone(), zero.clone()),
3699 ThreeMomentum::new(-large + f(2), zero.clone(), zero.clone()),
3700 ]);
3701 let t = F(T::from_f64_exact_binary(0.1));
3702 for (offset, mass) in [(0, 0), (3, 0), (3, 4)] {
3703 let center = LoopMomenta::from_iter([
3704 ThreeMomentum::new(f(offset), zero.clone(), zero.clone()),
3705 zero_vector.clone(),
3706 ]);
3707 let mut case_masses = masses.clone();
3708 case_masses[EdgeIndex(3)] = f(mass);
3709 let represented =
3710 cancellation.routed_ray(&velocity, ¢er, &externals, &case_masses, lmb);
3711 let prepared = represented.evaluate(&t);
3712 let original = cancellation.compute_self_and_r_derivative(
3713 &t,
3714 &velocity,
3715 ¢er,
3716 &externals,
3717 &case_masses,
3718 lmb,
3719 );
3720 let prepared_p = f(2) * &t + f(offset);
3721 let prepared_e = (prepared_p.square() + f(mass).square()).sqrt();
3722 let tolerance = one.epsilon() * f(128);
3723 assert!((&prepared.0 - &prepared_e).abs() < tolerance);
3724 assert!((&prepared.1 - f(2) * &prepared_p / &prepared_e).abs() < tolerance);
3725 if one.epsilon() > &one / f(10).powi(20) {
3726 let original_p = &one / f(8) + f(offset);
3727 let original_e = (original_p.square() + f(mass).square()).sqrt();
3728 assert!((&original.0 - &original_e).abs() < tolerance);
3729 assert!((&original.1 - f(2) * original_p / original_e).abs() < tolerance);
3730 assert!((&prepared.0 - &original.0).abs() > &one / f(100));
3731 } else {
3732 assert!((&original.0 - &prepared.0).abs() < tolerance);
3735 assert!((&original.1 - &prepared.1).abs() < tolerance);
3736 }
3737 }
3738
3739 let mut endpoint = surface.clone();
3742 endpoint.energies = vec![EdgeIndex(1), EdgeIndex(2)];
3743 masses[EdgeIndex(1)] = zero.clone();
3744 masses[EdgeIndex(2)] = f(3);
3745 let velocity = LoopMomenta::from_iter([
3746 ThreeMomentum::new(f(3), f(4), zero.clone()),
3747 zero_vector.clone(),
3748 ]);
3749 let mut shifted_center = LoopMomenta::from_iter([
3750 zero_vector.clone(),
3751 ThreeMomentum::new(f(4), zero.clone(), zero.clone()),
3752 ]);
3753 let (value, derivative) = endpoint.compute_self_and_r_derivative(
3754 &zero,
3755 &velocity,
3756 &shifted_center,
3757 &externals,
3758 &masses,
3759 lmb,
3760 );
3761 assert_eq!(value, f(5) - &e_cm);
3762 assert_eq!(derivative, f(5));
3763 let prepared_endpoint = endpoint
3764 .routed_ray(&velocity, &shifted_center, &externals, &masses, lmb)
3765 .evaluate(&zero);
3766 assert_eq!(prepared_endpoint, (f(5) - &e_cm, f(5)));
3767 let stationary = LoopMomenta::from_iter([zero_vector.clone(), zero_vector]);
3768 assert_eq!(
3769 endpoint
3770 .compute_self_and_r_derivative(
3771 &zero,
3772 &stationary,
3773 &shifted_center,
3774 &externals,
3775 &masses,
3776 lmb,
3777 )
3778 .1,
3779 zero
3780 );
3781 assert!(matches!(
3782 endpoint.solve_lu_cut(
3783 &stationary,
3784 &externals,
3785 &masses,
3786 lmb,
3787 &e_cm,
3788 &mut diagnostics,
3789 &identity,
3790 ),
3791 Err(SafeguardedNewtonError::InvalidOutside { .. })
3792 ));
3793
3794 let tiny = &one / f(10).powi(200);
3797 masses[EdgeIndex(1)] = tiny.clone();
3798 let derivative = endpoint
3799 .compute_self_and_r_derivative(
3800 &zero,
3801 &velocity,
3802 &shifted_center,
3803 &externals,
3804 &masses,
3805 lmb,
3806 )
3807 .1;
3808 let prepared_derivative = endpoint
3809 .routed_ray(&velocity, &shifted_center, &externals, &masses, lmb)
3810 .evaluate(&zero)
3811 .1;
3812 for derivative in [derivative, prepared_derivative] {
3813 if tiny.square() == zero {
3814 assert!(derivative.is_nan() || derivative.is_infinite());
3815 } else {
3816 assert_eq!(derivative, zero);
3817 }
3818 }
3819 masses[EdgeIndex(1)] = zero.clone();
3820 for (axis, expected) in [3, 4, 0].into_iter().enumerate() {
3821 shifted_center[crate::momentum::sample::LoopIndex(0)] = ThreeMomentum::new(
3822 if axis == 0 {
3823 tiny.clone()
3824 } else {
3825 zero.clone()
3826 },
3827 if axis == 1 {
3828 tiny.clone()
3829 } else {
3830 zero.clone()
3831 },
3832 if axis == 2 {
3833 tiny.clone()
3834 } else {
3835 zero.clone()
3836 },
3837 );
3838 let derivative = endpoint
3839 .compute_self_and_r_derivative(
3840 &zero,
3841 &velocity,
3842 &shifted_center,
3843 &externals,
3844 &masses,
3845 lmb,
3846 )
3847 .1;
3848 let prepared_derivative = endpoint
3849 .routed_ray(&velocity, &shifted_center, &externals, &masses, lmb)
3850 .evaluate(&zero)
3851 .1;
3852 for derivative in [derivative, prepared_derivative] {
3853 if tiny.square() == zero {
3854 assert!(derivative.is_nan() || derivative.is_infinite());
3855 } else {
3856 assert!((derivative - f(expected)).abs() < one.epsilon() * f(128));
3857 }
3858 }
3859 }
3860 }
3861 check_native::<f64>(&graph, &surface);
3862 check_native::<QuadFloat>(&graph, &surface);
3863 check_native::<ArbPrec>(&graph, &surface);
3864 }
3865
3866 #[test]
3867 fn canonical_physical_lu_root_resolves_retained_bracket_endpoints() {
3868 use crate::graph::FeynmanGraph;
3869
3870 test_initialise().unwrap();
3871 let point_tokens = [
3876 "-4877.19776949134799589974803772414639433359513358235797727662621078949562551592647720875226397943918395045206756176248678175688837208619050791653582445021695271333556149595551567501518142933108041137862389137548159866615474120165599255689473831877256382039353906809830046294831502842769433115811496491403",
3877 "-4788.44733559297503338424048189604502643310578502030319826258363405583106128848247171958913511942781252928660066253754883065799513281132098117063664934324987293459853837736080937683626624511055230677016357961716720986212711446609804288980413634695779067271806710579027258896440419592959858535084502130291",
3878 "3449.85408446744743054600317691517351068755983604965291557283080813569562933280322362036960861849284721431171841172810290687504905161462871600062758043010601204035005302723970251590824854187235628530596853916572662417827550093988644061689354024522549573776864100886485399680047404526188593294956650329940",
3879 "70.3244241810218751702648147574915682722146332904085090192206088762519914112850473659048343153323638641871729435254245633882918549420905038752476781927385371433230303313665642902999745355570884098566729530821574404942919072073238328487435044514999060063382762063688077567584466375120955594649302749638168",
3880 "-64.7328550651196388632156519795545666707240287311537237777794147531761978347671932403072110940638881039840834582472804137358931989273824856974894575353144549925155422489503341679707811903159498611335677205735964537298380313103709897338608109368125932232793979426015474979910153144543330056886932939528991",
3881 "-169.977204441411724276834141865099406683621147453741490083449379145954509733051600240055550798830052895001473550375457779362103819932642751869589067397325831680056922569024797824237561594653585076675793132823695793970660453958165008987223868047366419040597994417711631570323172275534429627197335733541788",
3882 "-5600.61087887521441317726380154507077804125909265810807210495844671671183617788774688843570145751202477847145016280355634030258604974973758865789530484255123560331590332195975778091351865880370425224431732203854524561456548571792312894398114116052933817481418085818812657379218499842947239271592857749704",
3883 "-4836.61374618501855280967146959499945066804232727342341015614259662078809271214898964547576447118364148478316361672773076137899768111124298404537445866753685338684537418353640716742530483139927834572180382685089716954278888134794244645196893419036539682711826176677044169785265849490484473737712006654560",
3884 "2145.99486696308129706260024015689439317496385917360897844622508358240397624209431777021609681539543126318894513773663359142436896535260327930425968202779550650784950814483566205233631415943233817778512294707166462039932281821355935955750715055287115993275252104683925707031965340569344306593459551977201",
3885 "-5604.88372374262804889294640877016787850008172170528245661491078660880869657816973625663719378778526660563427937564485272926173353092285235941079848809391075384808960803275406005472826675962967469084057925785326996659483564308288383384251734955274712244877309369072980850467041673500458058150368522257495",
3886 "-4334.45541003902767715452592577800941446238649756064773722205221184221360633788778921553682091482858995046598733903274399368838883790258941797495609397561874360921914142839313072883007139115101612942134481476731356575319298406454126144036222827728908340679754158679444863572311869978356712039675439410147",
3887 "2429.79731946564390222685915546149324045671034707482980645420787377878023354031617617014193097522475262733934128132427049128461231717208810751567187554211866137651923930428300030230687501307593042508234180738169107139713264377038173966078706808479870077056431717387171214082081679052824398764469662844001",
3888 ];
3889 let point = point_tokens
3890 .iter()
3891 .map(|token| {
3892 let value = token.parse::<ArbPrec>().unwrap();
3893 assert_eq!(value.to_string(), *token);
3894 F(value)
3895 })
3896 .collect_vec();
3897 let one = point[0].one();
3898 let zero = one.zero();
3899 let model = crate::utils::load_generic_model("sm");
3900 let graph: Graph = include_str!("../../../../tests/resources/graphs/GL638.dot")
3901 .into_graph(&model)
3902 .unwrap();
3903 let lmb = &graph.loop_momentum_basis;
3904 assert_eq!(
3905 lmb.loop_edges.raw,
3906 vec![EdgeIndex(3), EdgeIndex(4), EdgeIndex(7), EdgeIndex(10)]
3907 );
3908 let surface = Esurface {
3909 energies: vec![EdgeIndex(2), EdgeIndex(6), EdgeIndex(12), EdgeIndex(13)],
3910 external_shift: vec![(EdgeIndex(0), -1), (EdgeIndex(1), -1)],
3911 vertex_set: VertexSet::dummy(),
3912 };
3913 let masses = graph.get_real_mass_vector::<ArbPrec>(&model);
3914 for (edge, mass) in [(2, 125), (6, 173), (12, 173), (13, 0)] {
3915 assert_eq!(masses[EdgeIndex(edge)], one.from_i64(mass));
3916 }
3917 let externals = ExternalFourMomenta::from_iter([1, -1].map(|sign| {
3918 FourMomentum::from_args(
3919 one.from_i64(500),
3920 zero.clone(),
3921 zero.clone(),
3922 one.from_i64(500 * sign),
3923 )
3924 }));
3925 assert_eq!(externals.len(), lmb.ext_edges.len());
3926 let momentum = LoopMomenta::from_iter(
3927 point
3928 .as_chunks::<3>()
3929 .0
3930 .iter()
3931 .map(|v| ThreeMomentum::new(v[0].clone(), v[1].clone(), v[2].clone())),
3932 );
3933 let center = LoopMomenta::from_iter(
3934 (0..4).map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
3935 );
3936 let e_cm = one.from_i64(1000);
3937 let identity =
3938 RadialRootIdentity::new("canonical physical overlap graph 'GL638' cut group 3".into());
3939 let ray = surface.routed_ray(&momentum, ¢er, &externals, &masses, lmb);
3940 let guess = Esurface::radius_guess_from_terms(
3941 &ray.shift,
3942 ray.energies
3943 .iter()
3944 .map(|(_, v, b, _)| (v.norm_squared(), v.clone() * b)),
3945 );
3946 let strict = safeguarded_newton_iteration_and_derivative(
3947 &zero,
3948 &guess,
3949 |r| ray.evaluate(r),
3950 &one,
3951 2000,
3952 64,
3953 &e_cm,
3954 )
3955 .unwrap_err();
3956 assert_eq!(
3957 format!(
3958 "sampling root is not certified at the current precision: {identity}: {strict:?}"
3959 ),
3960 "sampling root is not certified at the current precision: canonical physical overlap graph 'GL638' cut group 3: DidNotConverge { result: NewtonIterationResult { solution: F(VarFloat { float: 4.50096019182270793871156294720615053060112718361364618170932397734514711080987392331940322755100771019674059655008057236752740928154857006112161128562114855544422368074316508870600192219593565649427544978773164779121432253631794038806540811255813327701451237828689832176990555566921873234752788019943785e-2 }), derivative_at_solution: F(VarFloat { float: 15947.7515847915197382229906009716238684820639052766863102939247189292647433796806064815357943825914286672680520425869860307247589322869726693916868899181132224865883496823643904736774712938449859097464691001739892743096176224973672876047140085992981227937651521443373989485856759341750304597183394266484 }), error_of_function: F(VarFloat { float: -2.86698583604188839625755508139156634506370492325388705163790645185321035051758274904404068484469074644867580906595607334158997766559958751695315195045047528315210956637693486164171287342869042640210070772612642749923485444545310860535350764889727519890646541984079814999901126931044646673003826046088028e-298 }), num_iterations_used: 10 }, lower_bound: F(VarFloat { float: 4.50096019182270793871156294720615053060112718361364618170932397734514711080987392331940322755100771019674059655008057236752740928154857006112161128562114855544422368074316508870600192219593565649427544978773164779121432253631794038806540811255813327701451237828689832176990555566921873234752788019943785e-2 }), upper_bound: F(VarFloat { float: 4.50096019182270793871156294720615053060112718361364618170932397734514711080987392331940322755100771019674059655008057236752740928154857006112161128562114855544422368074316508870600192219593565649427544978773164779121432253631794038806540811255813327701451237828689832176990555566921873234752788019943844e-2 }) }"
3961 );
3962 let (_, production) = surface
3963 .solve_lu_cut(
3964 &momentum,
3965 &externals,
3966 &masses,
3967 lmb,
3968 &e_cm,
3969 &mut RadialRootDiagnostics::default(),
3970 &identity,
3971 )
3972 .unwrap();
3973 let production_budget =
3976 one.from_i64((4 * ray.energies.len() + 1) as i64) * one.epsilon() * &e_cm;
3977 let (production_value, production_derivative) = ray.evaluate(&production.solution);
3978 assert_eq!(production.error_of_function, production_value);
3979 assert_eq!(production.derivative_at_solution, production_derivative);
3980 assert!(production.error_of_function.abs() <= production_budget);
3981 let certified = RadialRootDiagnostics::default()
3982 .solve(
3983 &identity,
3984 &zero,
3985 &guess,
3986 |r| ray.evaluate(r),
3987 &one,
3988 2000,
3989 64,
3990 &e_cm,
3991 )
3992 .unwrap();
3993 let SafeguardedNewtonError::DidNotConverge {
3994 result,
3995 lower_bound,
3996 upper_bound,
3997 } = strict
3998 else {
3999 panic!("retained physical source did not reproduce an exhausted bracket");
4000 };
4001 assert_eq!(result.solution, lower_bound);
4002 assert_eq!(result.num_iterations_used, 10);
4003 assert!(upper_bound > lower_bound);
4004 let midpoint = (&lower_bound + &upper_bound) / one.from_i64(2);
4005 assert!(midpoint == lower_bound || midpoint == upper_bound);
4006 let width = &upper_bound - &lower_bound;
4007 let budget = one.epsilon() * &e_cm;
4008 assert_eq!(certified.solution, upper_bound);
4009 assert_eq!(certified.num_iterations_used, result.num_iterations_used);
4010 let (upper_value, upper_derivative) = ray.evaluate(&upper_bound);
4011 assert_eq!(certified.error_of_function, upper_value);
4012 assert_eq!(certified.derivative_at_solution, upper_derivative);
4013 assert!(
4016 result.error_of_function.abs() > &budget + result.derivative_at_solution.abs() * &width
4017 );
4018 assert!(certified.error_of_function.abs() > budget);
4019 assert!(
4020 certified.error_of_function.abs()
4021 <= &budget + certified.derivative_at_solution.abs() * &width
4022 );
4023 crate::debug_tags!(#integration, #cut, #solver;
4024 stage = "canonical_physical_lu_root_setup",
4025 cut_group = 3, physical_cut = 0,
4026 initial_guess = %guess, original_residual_budget = %budget,
4027 bracket_width = %width, file.ray = ?ray,
4028 file.original_momenta = ?momentum, file.original_result = ?result,
4029 "actual captured physical ray and unchanged LU solver policy"
4030 );
4031 for (endpoint, radius) in [("lower", &lower_bound), ("upper", &upper_bound)] {
4032 let (value, derivative) = ray.evaluate(radius);
4033 let enclosed = surface
4034 .evaluate_routed_enclosed(radius, &momentum, &externals, &masses, lmb)
4035 .unwrap();
4036 assert!(!derivative.is_nan() && !derivative.is_infinite() && derivative > zero);
4037 assert!(enclosed.iter().all(|value| value.is_finite()));
4038 let resolution_budget = &budget + derivative.abs() * &width;
4039 let allowed = budget.0.mpfr_enclosure(2048).0;
4040 let native_strict = value.abs() <= budget;
4041 let native_resolution = value.abs() <= resolution_budget;
4042 let original_strict = enclosed[0] >= -allowed.clone() && enclosed[1] <= allowed;
4043 assert!(
4044 original_strict,
4045 "{endpoint} violates the original equation budget"
4046 );
4047 crate::debug_tags!(#integration, #cut, #solver;
4048 stage = "canonical_physical_lu_root_endpoint", endpoint,
4049 radius = %radius, native_residual = %value, derivative = %derivative,
4050 original_residual_lower = %enclosed[0], original_residual_upper = %enclosed[1],
4051 original_residual_budget = %budget, resolution_budget = %resolution_budget,
4052 native_strict, native_resolution, original_strict,
4053 "both represented endpoints satisfy the unchanged original equation budget"
4054 );
4055 }
4056
4057 let inconsistent_derivative = |radius: &F<ArbPrec>| {
4061 let (value, derivative) = ray.evaluate(radius);
4062 let derivative = if radius == &upper_bound {
4063 derivative * one.from_i64(8)
4064 } else {
4065 derivative
4066 };
4067 (value, derivative)
4068 };
4069 let invalid_strict = safeguarded_newton_iteration_and_derivative(
4070 &zero,
4071 &guess,
4072 inconsistent_derivative,
4073 &one,
4074 2000,
4075 64,
4076 &e_cm,
4077 )
4078 .unwrap_err();
4079 let SafeguardedNewtonError::DidNotConverge {
4080 result: invalid_result,
4081 lower_bound: invalid_lower,
4082 upper_bound: invalid_upper,
4083 } = &invalid_strict
4084 else {
4085 panic!("adversarial callback changed the structural failure");
4086 };
4087 assert!(invalid_result.num_iterations_used >= 4);
4088 assert_eq!(invalid_result.solution, lower_bound);
4089 assert_eq!(*invalid_lower, lower_bound);
4090 assert_eq!(*invalid_upper, upper_bound);
4091 let (alternate_value, wrong_derivative) = inconsistent_derivative(&upper_bound);
4092 assert!(alternate_value.abs() <= &budget + wrong_derivative * &width);
4093 let rejected = RadialRootDiagnostics::default()
4094 .solve(
4095 &RadialRootIdentity::new("adversarial alternate endpoint derivative".into()),
4096 &zero,
4097 &guess,
4098 inconsistent_derivative,
4099 &one,
4100 2000,
4101 64,
4102 &e_cm,
4103 )
4104 .unwrap_err();
4105 assert_eq!(format!("{rejected:?}"), format!("{invalid_strict:?}"));
4106 }
4107
4108 #[test]
4109 fn phase_space_cut_root_replays_canonical_foreign_inverse_cancellation() {
4110 test_initialise().unwrap();
4111 let point_tokens = [
4116 "1367030.43662744423077657886622585118732404500993473554913895566241589500405947461690714412624268513505631015396234810943269596104532748643105542702039862197867565164412838526283892048404678602045626445614508293241427506110755435656591802035619036221554197413841701638040025597247140012517189434530153459",
4117 "-999526.399472714004924745558487742174280959297534606425446121987615879279562127703414975258631734220750778229983972859653308430370782154297373722895399005861599636821957013566636477314723339751660664618818230868728997962470940365780821107652219318608507461393737546471739313569778449847787262829689519758",
4118 "2635.11804627574968679150746698546684926537860473844219904054202664480773875967585945065405808941879393444480927960481154917330356034222219635456028440278614622109927632916893794632929631394563782912542725019630912377572312073034188155233244619198043408564989307377224208055269346704158128964908317509261",
4119 "1365298.80606570193439178248903075440797721030001172865681315316962534564202453943423169414748443288269134401947007088168439700475928251218359436660404679423063978012317674014693339414507122222142523195226338848798195882508017496231651900921236382771006795390348616128129934316532814543030209744364019054",
4120 "-998820.613196962571373332987243603087494541655288997511144379181267051699516068044611894994190494668286551419090745020640141977947029490154255331734395335596048929725132710856235917568105358581132488124086315318516657933430936244305844004808412923698398375511933676277573855820903335530154557900187401664",
4121 "4047.33414152831088829375438466435223275171308987396130932582985387723022229862922976703274469623350667935654058159331604678380195069378771816145293238142522165286453283051283913992888932063257542647725648351805459013522266494043094042838131736080143438861266226902012570363568976019577762021664023114866",
4122 "83.1126856152680405091850152707032759635065978000760419277583832932540620572340228677113831897018166702219588733109015730699449632190446808998629297952866898031107347012837750524595236018672538656817674901345397345906420467235996584856394111008677691361716531755180039690828818424181738021720937612332473",
4123 "-271.487004006624936032383971262627992814142127178531082780852027992007247020700756462748911221240546859750792472835768342005119164451772938283340118890996475035825441694527852209889256539059182981432179743946981544937115591798425012272630650241061657324339012716835440520492257904951141777212048569398409",
4124 "172.995634312193812329535193383815352801038985837137105437759642412099825918135903635674987213191903064253661504641496794322344612352796885741009454426914017751085087430094408938579737245424065728815449873693652886682643449058779221848518639106186622340605844416298128898323757003199986295961514068931809",
4125 "1553.02030744971213963777112883628816116222224922513300779830478460724114360528704175205365061869941782426355915003669694077799625540057885933310993881768684532880891858480897867129984665510634951301156097819370899150217888483360555670282615505920358106216430177949458381682435533431403640861028867643634",
4126 "175.729482877721774403936070331243420758342427008390700023593859226796553067648366117048302143782877238854817038614857585311307016571639203050561588272527571998881087153974255763006207376888422035958124493436136546263533358525466966789793340591585338882735253021052931213562265925007864963381639808647379",
4127 "-987.094530658251250328865816150283711625817471698266080780545993723465686910923865574545177650018084715406989468479547700982468885609732012850096019949134333598256299704715871688857759497730140969322324491488236509562871514705347225367092074540791495561129260238451255593578254459645239533939527551314165",
4128 ];
4129 let point = point_tokens
4130 .iter()
4131 .map(|token| {
4132 let value = token.parse::<ArbPrec>().unwrap();
4133 assert_eq!(value.to_string(), *token);
4134 F(value)
4135 })
4136 .collect_vec();
4137 let one = point[0].one();
4138 let zero = one.zero();
4139 let f = |value| one.from_i64(value);
4140 let model = crate::utils::load_generic_model("sm");
4141 let graph: Graph = include_str!("../../../../tests/resources/graphs/GL638.dot")
4142 .into_graph(&model)
4143 .unwrap();
4144 let lmb = &graph.loop_momentum_basis;
4145 assert_eq!(
4146 lmb.loop_edges.raw,
4147 vec![EdgeIndex(3), EdgeIndex(4), EdgeIndex(7), EdgeIndex(10)]
4148 );
4149 for (edge, coefficients) in [(2, [-1, 1, 0, 0]), (6, [-1, 1, 0, 1]), (10, [0, 0, 0, 1])] {
4150 assert_eq!(
4151 lmb.edge_signatures[EdgeIndex(edge)]
4152 .internal
4153 .to_momtrop_format(),
4154 coefficients
4155 );
4156 }
4157 let surface = Esurface {
4158 energies: vec![EdgeIndex(2), EdgeIndex(6), EdgeIndex(10)],
4159 external_shift: vec![(EdgeIndex(0), -1), (EdgeIndex(1), -1)],
4160 vertex_set: VertexSet::dummy(),
4161 };
4162 let masses = graph
4163 .underlying
4164 .new_edgevec_from_iter(lmb.edge_signatures.iter().map(|(edge, _)| {
4165 f(match edge.0 {
4166 2 => 125,
4167 6 | 10 => 173,
4168 _ => 0,
4169 })
4170 }))
4171 .unwrap();
4172 let externals = ExternalFourMomenta::from_iter([1, -1].map(|sign| {
4173 FourMomentum::from_args(f(500), zero.clone(), zero.clone(), f(500 * sign))
4174 }));
4175 assert_eq!(externals.len(), lmb.ext_edges.len());
4176 let center = LoopMomenta::from_iter(
4177 (0..4).map(|_| ThreeMomentum::new(zero.clone(), zero.clone(), zero.clone())),
4178 );
4179 let radius = point
4182 .iter()
4183 .map(F::square)
4184 .fold(zero.clone(), |sum, x| sum + x)
4185 .sqrt();
4186 let direction = point.iter().map(|x| x / &radius).collect_vec();
4187 let velocity = LoopMomenta::from_iter(
4188 direction
4189 .as_chunks::<3>()
4190 .0
4191 .iter()
4192 .map(|v| ThreeMomentum::new(v[0].clone(), v[1].clone(), v[2].clone())),
4193 );
4194 let original = |r: &F<ArbPrec>| {
4195 surface.compute_self_and_r_derivative(r, &velocity, ¢er, &externals, &masses, lmb)
4196 };
4197 let ray = surface.routed_ray(&velocity, ¢er, &externals, &masses, lmb);
4198 let routed = |r: &F<ArbPrec>| ray.evaluate(r);
4199 let scale = original(&zero).0.abs().max(one.clone());
4200 assert_eq!(scale, f(529));
4201 let guess = f(300);
4202 let tolerance = f(64);
4203 let identity = RadialRootIdentity::new("canonical foreign phase-space Cut1".into());
4204 let old_strict = safeguarded_newton_iteration_and_derivative(
4205 &zero, &guess, original, &tolerance, 2048, 96, &scale,
4206 );
4207 let old_checked = RadialRootDiagnostics::default().solve(
4208 &identity, &zero, &guess, original, &tolerance, 2048, 96, &scale,
4209 );
4210 let ray_strict = safeguarded_newton_iteration_and_derivative(
4211 &zero, &guess, routed, &tolerance, 2048, 96, &scale,
4212 );
4213 let ray_checked = RadialRootDiagnostics::default().solve(
4214 &identity, &zero, &guess, routed, &tolerance, 2048, 96, &scale,
4215 );
4216 crate::debug_tags!(#integration, #sampling, #solver;
4217 stage = "canonical_foreign_cut_root_comparison",
4218 scale = %scale, tolerance = %tolerance,
4219 old_strict = ?old_strict, old_checked = ?old_checked,
4220 ray_strict = ?ray_strict, ray_checked = ?ray_checked,
4221 "same native direction and root budget; only radial routing association differs"
4222 );
4223 let Err(SafeguardedNewtonError::DidNotConverge {
4224 result,
4225 lower_bound,
4226 upper_bound,
4227 }) = old_strict
4228 else {
4229 panic!("retained point did not reproduce the original failed callback");
4230 };
4231 assert_eq!(result.solution, lower_bound);
4232 assert_eq!(result.solution, F("397339.292129129457670657255502631235707430728003236007665188851741059988603886168137387085943384940050545361933742066907875904266411120132268928043162217340042566193200203939814362605526391210977153331875750728687297380447984755419924107738341193852462036361671032388546985919083686130672457601933412077".parse::<ArbPrec>().unwrap()));
4233 assert_eq!(result.derivative_at_solution, F("1.89172234332619088523547124801376213069340504387632086738281927967636989017896303335256085095364995340008004517730539670922548200475769065127437640250637540730631026978180801048759551386539060201137497670459856172911183019727298113312180354967670498695170211446748295063372406403797004835125320002494423e-3".parse::<ArbPrec>().unwrap()));
4234 assert_eq!(result.error_of_function, F("-1.14679433441675535850302203255662653802548196930155482065516258074128414020703309961761627393787629857947032362638242933663599106623983500678126078018019011326084382655077394465668514937147617056084028309045057099969394177818124344214140305955891007956258616793631925999960450772417858669201530418435211e-296".parse::<ArbPrec>().unwrap()));
4235 assert_eq!(result.num_iterations_used, 17);
4236 assert!(upper_bound > lower_bound);
4237 assert!(result.error_of_function.abs() > one.epsilon() * &tolerance * &scale);
4238 assert!(old_checked.is_err());
4241 let prepared = ray_strict.expect("pre-routed same-budget comparison did not converge");
4242 let checked = ray_checked.unwrap();
4243 assert_eq!(prepared.solution, checked.solution);
4244 assert!(prepared.derivative_at_solution > zero);
4245 assert!(prepared.error_of_function.abs() <= one.epsilon() * &tolerance * &scale);
4246 let residual = surface
4247 .evaluate_routed_enclosed(&prepared.solution, &velocity, &externals, &masses, lmb)
4248 .unwrap();
4249 let allowed = (one.epsilon() * &tolerance * &scale)
4250 .0
4251 .mpfr_enclosure(2048)
4252 .0;
4253 assert!(residual[0] >= -allowed.clone() && residual[1] <= allowed);
4254 crate::debug_tags!(#integration, #sampling, #solver;
4255 stage = "canonical_foreign_cut_original_residual",
4256 native_root = %prepared.solution,
4257 original_residual_lower = %residual[0], original_residual_upper = %residual[1],
4258 "directed original routing at the pre-routed callback root"
4259 );
4260 }
4261
4262 #[test]
4263 fn implicit_sampling_map_uses_graph_esurface_root_and_roundtrips() {
4264 test_initialise().unwrap();
4265 let graph: Graph = dot!(digraph implicit_sampling_esurface {
4266 ext [style=invis]
4267 node [num=1]
4268 edge [num=1 mass=0]
4269 ext -> a:0 [id=0]
4270 a -> b [id=1 lmb_id=0]
4271 a -> b [id=2 lmb_id=1]
4272 a -> b [id=3]
4273 b:1 -> ext [id=4]
4274 })
4275 .unwrap();
4276 let lmb = graph.loop_momentum_basis.clone();
4277 let surface = Esurface {
4278 energies: vec![EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)],
4279 external_shift: vec![(EdgeIndex(0), -1)],
4280 vertex_set: VertexSet::dummy(),
4281 };
4282 let masses = graph
4283 .underlying
4284 .new_edgevec_from_iter([F(0.0), F(173.0), F(173.0), F(125.0), F(0.0)])
4285 .unwrap();
4286 let externals = ExternalFourMomenta::from_iter(
4287 [FourMomentum::from_args(F(1000.0), F(0.0), F(0.0), F(0.0)); 2],
4288 );
4289 let map = surface
4290 .sampling_radial_map(&lmb, &masses, &externals, 400.0, 2.0)
4291 .unwrap();
4292 for radial_coordinate in [0.1, 0.9] {
4295 let coordinates = [radial_coordinate, 0.27, 0.61, 0.39, 0.72, 0.58];
4296 let forward = map.forward(&coordinates).unwrap();
4297 let step = 1.0e-5;
4298 let mut derivative_matrix = vec![vec![0.0; 6]; 6];
4299 for axis in 0..6 {
4300 let mut plus = coordinates;
4301 let mut minus = coordinates;
4302 plus[axis] += step;
4303 minus[axis] -= step;
4304 let plus = map.forward(&plus).unwrap();
4305 let minus = map.forward(&minus).unwrap();
4306 for (component, row) in derivative_matrix.iter_mut().enumerate().take(6) {
4307 row[axis] = (plus.point[component] - minus.point[component]) / (2.0 * step);
4308 }
4309 }
4310 let numerical_jacobian = SamplingMapAffine::new(derivative_matrix, vec![0.0; 6])
4311 .unwrap()
4312 .determinant();
4313 assert!((numerical_jacobian / forward.jacobian - 1.0).abs() < 1.0e-4);
4314
4315 assert!(
4316 forward
4317 .diagnostics
4318 .iter()
4319 .any(|diagnostic| diagnostic == "implicit_surface:regular_root")
4320 );
4321 assert!(forward.jacobian.is_finite() && forward.jacobian > 0.0);
4322 let inverse = map.inverse(&forward.point).unwrap();
4323 assert!(inverse.residual < 1.0e-9, "{}", inverse.residual);
4324 for (actual, expected) in inverse.coordinates.iter().zip(coordinates) {
4325 assert!((actual - expected).abs() < 1.0e-9);
4326 }
4327 }
4328
4329 let one = F::<crate::utils::f128>::from_f64(1.0);
4333 let energy = one.from_i64(1000);
4334 let displacement = one.from_i64(10).powi(-20);
4335 assert_eq!(energy.into_f64(), (energy + displacement).into_f64());
4336 let native_masses = graph
4337 .underlying
4338 .new_edgevec_from_iter(
4339 masses
4340 .iter()
4341 .map(|(_, mass)| F::<crate::utils::f128>::from_ff64(*mass)),
4342 )
4343 .unwrap();
4344 let coordinates = [0.19, 0.27, 0.61, 0.39, 0.72, 0.58]
4345 .map(|value| F::<crate::utils::f128>::from_f64(value).0);
4346 let mut points = Vec::new();
4347 for energy in [energy, energy + displacement] {
4348 let externals = ExternalFourMomenta::from_iter(
4349 [FourMomentum::from_args(energy, one.zero(), one.zero(), one.zero()); 2],
4350 );
4351 let map = surface
4352 .sampling_radial_map(&lmb, &native_masses, &externals, 400.0, 2.0)
4353 .unwrap();
4354 let forward = map.forward(&coordinates).unwrap();
4355 let inverse = map.inverse(&forward.point).unwrap();
4356 for (actual, expected) in inverse.coordinates.iter().zip(coordinates) {
4357 assert!((F(*actual) - F(expected)).abs() < one.from_i64(10).powi(-25));
4358 }
4359 points.push(forward.point);
4360 }
4361 assert_ne!(points[0], points[1]);
4362 assert_eq!(
4363 points[0]
4364 .iter()
4365 .map(|value| F(*value).into_f64())
4366 .collect::<Vec<_>>(),
4367 points[1]
4368 .iter()
4369 .map(|value| F(*value).into_f64())
4370 .collect::<Vec<_>>(),
4371 );
4372 }
4373
4374 #[test]
4375 fn classification_preserves_the_previous_existing_predicate() {
4376 let e_cm = F(10.0);
4377 let shift_tolerance = F(ESURFACE_SHIFT_THRESHOLD) * e_cm;
4378 let normalized_margin_tolerance = F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD);
4379 let invariant_tolerance = normalized_margin_tolerance * e_cm * e_cm;
4380
4381 for shift_factor in [-2, -1, 0, 1, 2] {
4382 let shift_part = shift_tolerance * shift_tolerance.from_i64(shift_factor);
4383 for margin_factor in [-2, -1, 0, 1, 2] {
4384 let invariant_margin =
4385 invariant_tolerance * invariant_tolerance.from_i64(margin_factor);
4386 let was_existing =
4387 shift_part < -&shift_tolerance && invariant_margin > invariant_tolerance;
4388 let classification = Esurface::classify_invariant_margin(
4389 &shift_part,
4390 invariant_margin,
4391 &e_cm,
4392 &normalized_margin_tolerance,
4393 );
4394
4395 assert_eq!(
4396 classification.is_existing(),
4397 was_existing,
4398 "existence changed for shift factor {shift_factor} and margin factor {margin_factor}",
4399 );
4400 }
4401 }
4402 }
4403
4404 #[test]
4405 fn invalid_programmatic_tolerances_cannot_invert_classification() {
4406 let e_cm = F(10.0);
4407 let shift_part = F(-1.0);
4408 let invariant_margin = F(0.0);
4409
4410 for tolerance in [
4411 F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
4412 F(-DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
4413 F(f64::NAN),
4414 F(f64::INFINITY),
4415 ] {
4416 assert!(matches!(
4417 Esurface::classify_invariant_margin(
4418 &shift_part,
4419 invariant_margin,
4420 &e_cm,
4421 &tolerance,
4422 ),
4423 EsurfaceExistence::Pinched { .. }
4424 ));
4425 }
4426 }
4427
4428 #[test]
4429 fn positive_external_energy_filter_uses_energy_conservation() {
4430 let incoming_edges = (0..4).map(EdgeIndex::from).collect_vec();
4431 let outgoing_edges = (4..9).map(EdgeIndex::from).collect_vec();
4432 let shift_is_negative = |external_shift: &[(usize, i64)]| {
4433 Esurface {
4434 energies: vec![],
4435 external_shift: external_shift
4436 .iter()
4437 .map(|(edge, coefficient)| (EdgeIndex::from(*edge), *coefficient))
4438 .collect(),
4439 vertex_set: VertexSet::dummy(),
4440 }
4441 .external_shift_is_strictly_negative_for_positive_energies(
4442 &incoming_edges,
4443 &outgoing_edges,
4444 )
4445 };
4446
4447 assert!(
4448 shift_is_negative(&[(0, -1), (1, -1)]),
4449 "a negative proper subset of incoming energies must be retained"
4450 );
4451 assert!(
4452 shift_is_negative(&[(4, -1)]),
4453 "a negative proper subset of outgoing energies must be retained"
4454 );
4455 assert!(
4456 shift_is_negative(&[(0, -1), (1, -1), (2, -1), (3, -1), (4, 1)]),
4457 "energy conservation turns minus all incoming plus one outgoing into minus the remaining outgoing energies"
4458 );
4459 assert!(
4460 !shift_is_negative(&[
4461 (0, -1),
4462 (1, -1),
4463 (2, -1),
4464 (3, -1),
4465 (4, 1),
4466 (5, 1),
4467 (6, 1),
4468 (7, 1),
4469 (8, 1),
4470 ]),
4471 "the energy-conservation identity is zero rather than strictly negative"
4472 );
4473 assert!(
4474 !shift_is_negative(&[(0, -1), (4, 1)]),
4475 "a sign-indefinite difference must not be accepted from positivity alone"
4476 );
4477 assert!(
4478 !shift_is_negative(&[(9, -1)]),
4479 "a shift outside the external-energy partition must be rejected conservatively"
4480 );
4481 }
4482
4483 #[test]
4484 fn massless_two_to_two_surface_is_existing_away_from_collinear_pinch() {
4485 let dummy_graph = dummy_hedge_graph(4);
4486 let lmb = LoopMomentumBasis {
4487 tree: SuBitGraph::empty(0),
4488 loop_edges: vec![EdgeIndex::from(2)].into(),
4489 ext_edges: vec![].into(),
4490 edge_signatures: dummy_graph
4491 .new_edgevec_from_iter(vec![
4492 LoopExtSignature::from((vec![0], vec![1, 0])),
4493 LoopExtSignature::from((vec![0], vec![0, 1])),
4494 LoopExtSignature::from((vec![1], vec![0, 0])),
4495 LoopExtSignature::from((vec![-1], vec![-1, -1])),
4496 ])
4497 .unwrap(),
4498 };
4499 let esurface = Esurface {
4500 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
4501 external_shift: vec![(EdgeIndex::from(0), -1), (EdgeIndex::from(1), -1)],
4502 vertex_set: VertexSet::dummy(),
4503 };
4504 let masses = dummy_graph.new_edgevec_from_iter(vec![F(0.0); 4]).unwrap();
4505 let classify_pair = |second_spatial_momentum: (f64, f64), threshold: f64| {
4508 let external_momenta = ExternalFourMomenta::from_iter([
4509 FourMomentum::from_args(F(5.0), F(5.0), F(0.0), F(0.0)),
4510 FourMomentum::from_args(
4511 F(5.0),
4512 F(second_spatial_momentum.0),
4513 F(second_spatial_momentum.1),
4514 F(0.0),
4515 ),
4516 ]);
4517 esurface.classify_existence(&external_momenta, &lmb, &masses, &F(10.0), &F(threshold))
4518 };
4519
4520 assert!(matches!(
4521 classify_pair((0.0, 5.0), DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
4522 EsurfaceExistence::Existing { .. }
4523 ));
4524 assert!(matches!(
4525 classify_pair((5.0, 0.0), DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
4526 EsurfaceExistence::Pinched { .. }
4527 ));
4528 assert!(matches!(
4529 classify_pair((0.0, 5.0), 1.0),
4530 EsurfaceExistence::Pinched { .. }
4531 ));
4532 }
4533
4534 #[test]
4535 fn classifies_existing_pinched_and_non_existing_surfaces() {
4536 let dummy_graph = dummy_hedge_graph(5);
4537 let lmb = LoopMomentumBasis {
4538 tree: SuBitGraph::empty(0),
4539 loop_edges: vec![EdgeIndex::from(2), EdgeIndex::from(3)].into(),
4540 ext_edges: vec![].into(),
4541 edge_signatures: dummy_graph
4542 .new_edgevec_from_iter(vec![
4543 LoopExtSignature::from((vec![0, 0], vec![1])),
4544 LoopExtSignature::from((vec![0, 0], vec![-1])),
4545 LoopExtSignature::from((vec![1, 0], vec![0])),
4546 LoopExtSignature::from((vec![0, 1], vec![0])),
4547 LoopExtSignature::from((vec![1, 1], vec![-1])),
4548 ])
4549 .unwrap(),
4550 };
4551 let esurface = Esurface {
4552 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3), EdgeIndex::from(4)],
4553 external_shift: vec![(EdgeIndex::from(0), -1)],
4554 vertex_set: VertexSet::dummy(),
4555 };
4556 let masses = dummy_graph.new_edgevec_from_iter(vec![F(0.0); 5]).unwrap();
4557
4558 let classification = |energy| {
4559 let external_momenta = ExternalFourMomenta::from_iter([FourMomentum::from_args(
4560 F(energy),
4561 F(-10.0),
4562 F(0.0),
4563 F(0.0),
4564 )]);
4565 esurface.classify_existence(
4566 &external_momenta,
4567 &lmb,
4568 &masses,
4569 &F(10.0),
4570 &F(DEFAULT_ESURFACE_EXISTENCE_THRESHOLD),
4571 )
4572 };
4573
4574 assert!(matches!(
4575 classification(11.0),
4576 EsurfaceExistence::Existing { .. }
4577 ));
4578 assert!(matches!(
4579 classification(10.0),
4580 EsurfaceExistence::Pinched { .. }
4581 ));
4582 assert!(matches!(
4583 classification(10.0 + 1.0e-8),
4584 EsurfaceExistence::Pinched { .. }
4585 ));
4586 assert!(matches!(
4587 classification(9.0),
4588 EsurfaceExistence::NonExisting { .. }
4589 ));
4590 }
4591
4592 #[test]
4593 fn test_esurface() {
4594 let dummy_graph = dummy_hedge_graph(5);
4595
4596 let _energies_cache = dummy_graph
4597 .new_edgevec_from_iter([F(1.), F(2.), F(3.), F(4.), F(5.)])
4598 .unwrap();
4599
4600 let energies = vec![EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(2)];
4601
4602 let external_shift = vec![(EdgeIndex::from(3), 1), (EdgeIndex::from(4), 1)];
4603
4604 let mut esurface = Esurface {
4605 energies,
4606 external_shift,
4607 vertex_set: VertexSet::dummy(),
4608 };
4610
4611 let shift_rewrite = ShiftRewrite {
4612 dependent_momentum: EdgeIndex::from(4),
4613 dependent_momentum_expr: vec![
4614 (EdgeIndex::from(1), -1),
4615 (EdgeIndex::from(2), -1),
4616 (EdgeIndex::from(3), -1),
4617 ],
4618 };
4619
4620 esurface.canonicalize_shift(&shift_rewrite);
4621
4622 assert_eq!(
4623 esurface.external_shift,
4624 vec![(EdgeIndex::from(1), -1), (EdgeIndex::from(2), -1)]
4625 );
4626
4627 let energies = vec![EdgeIndex::from(0), EdgeIndex::from(2)];
4628
4629 let external_shift = vec![(EdgeIndex::from(1), -1)];
4630
4631 let _esurface = Esurface {
4632 energies,
4633 external_shift,
4634 vertex_set: VertexSet::dummy(),
4635 };
4637 }
4638
4639 #[test]
4640 fn test_add_external_shifts() {
4641 let shift_1 = vec![
4642 (EdgeIndex::from(0), 1),
4643 (EdgeIndex::from(1), 1),
4644 (EdgeIndex::from(2), -1),
4645 ];
4646 let shift_2 = vec![(EdgeIndex::from(1), -1), (EdgeIndex::from(2), 1)];
4647
4648 let add = add_external_shifts(&shift_1, &shift_2);
4649
4650 assert_eq!(add, vec![(EdgeIndex::from(0), 1)]);
4651
4652 let shift_3 = vec![(EdgeIndex::from(3), 1), (EdgeIndex::from(4), -1)];
4653 let shift_4 = vec![
4654 (EdgeIndex::from(0), 1),
4655 (EdgeIndex::from(1), 1),
4656 (EdgeIndex::from(2), 1),
4657 (EdgeIndex::from(4), 1),
4658 ];
4659
4660 let add = add_external_shifts(&shift_3, &shift_4);
4661
4662 assert_eq!(
4663 add,
4664 vec![
4665 (EdgeIndex::from(0), 1),
4666 (EdgeIndex::from(1), 1),
4667 (EdgeIndex::from(2), 1),
4668 (EdgeIndex::from(3), 1)
4669 ]
4670 );
4671 }
4672
4673 #[test]
4674 fn test_esurface_equality() {
4675 let esurface_1 = Esurface {
4676 energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
4677 external_shift: vec![(EdgeIndex::from(0), 1), (EdgeIndex::from(1), 1)],
4678 vertex_set: VertexSet::dummy(),
4679 };
4681
4682 let esurface_2 = Esurface {
4683 energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
4684 external_shift: vec![(EdgeIndex::from(0), 1), (EdgeIndex::from(1), 1)],
4685 vertex_set: VertexSet::dummy(),
4686 };
4688
4689 assert_eq!(esurface_1, esurface_2);
4690 }
4691
4692 #[test]
4693 fn test_to_atom() {
4694 let external_shift = vec![(EdgeIndex::from(1), -1)];
4695
4696 let esurface = Esurface {
4697 energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
4698 external_shift,
4699 vertex_set: VertexSet::dummy(),
4700 };
4702
4703 let esurface_atom = esurface.to_atom(&[]);
4704 let expected_atom = parse!("OSE(2) + OSE(3) - Q(1, spenso::cind(0))");
4705
4706 let diff = esurface_atom - &expected_atom;
4707 let diff = diff.expand();
4708 assert_eq!(diff, Atom::new());
4709 }
4710
4711 mod failing {
4712 use super::*;
4713
4714 #[test]
4715 fn test_from_cut_left_dt() {
4716 let mut hedge_graph_builder = HedgeGraphBuilder::new();
4717 let nodes = (0..4)
4718 .map(|_| hedge_graph_builder.add_node(()))
4719 .collect_vec();
4720
4721 hedge_graph_builder.add_edge(nodes[0], nodes[1], (), Orientation::Undirected);
4722 hedge_graph_builder.add_edge(nodes[0], nodes[2], (), Orientation::Undirected);
4723 hedge_graph_builder.add_edge(nodes[1], nodes[2], (), Orientation::Undirected);
4724 hedge_graph_builder.add_edge(nodes[1], nodes[3], (), Orientation::Undirected);
4725 hedge_graph_builder.add_edge(nodes[2], nodes[3], (), Orientation::Undirected);
4726
4727 hedge_graph_builder.add_external_edge(
4728 nodes[0],
4729 (),
4730 Orientation::Undirected,
4731 Flow::Sink,
4732 );
4733 hedge_graph_builder.add_external_edge(
4734 nodes[3],
4735 (),
4736 Orientation::Undirected,
4737 Flow::Source,
4738 );
4739
4740 let double_triangle: HedgeGraph<(), (), ()> =
4741 hedge_graph_builder.build::<NodeStorageVec<()>>();
4742 let node_0 = double_triangle.iter_crown(nodes[0]).into();
4743 let node_3 = double_triangle.iter_crown(nodes[3]).into();
4744
4745 let cuts = double_triangle.all_cuts(node_0, node_3);
4746
4747 let cross_section_cuts = cuts
4748 .into_iter()
4749 .map(|(node_l, cut, node_r)| CrossSectionCut {
4750 cut,
4751 left: node_l,
4752 right: node_r,
4753 })
4754 .map(|cut| Esurface::new_from_cut_left(&double_triangle, &cut, None))
4755 .collect_vec();
4756
4757 let expected_esurfaces = vec![
4758 Esurface {
4759 energies: vec![EdgeIndex::from(0), EdgeIndex::from(1)],
4760 external_shift: vec![(EdgeIndex::from(5), -1)],
4761 vertex_set: VertexSet::dummy(),
4762 },
4764 Esurface {
4765 energies: vec![EdgeIndex::from(0), EdgeIndex::from(2), EdgeIndex::from(4)],
4766 external_shift: vec![(EdgeIndex::from(5), -1)],
4767 vertex_set: VertexSet::dummy(),
4768 },
4770 Esurface {
4771 energies: vec![EdgeIndex::from(3), EdgeIndex::from(4)],
4772 external_shift: vec![(EdgeIndex::from(5), -1)],
4773 vertex_set: VertexSet::dummy(),
4774 },
4776 Esurface {
4777 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2), EdgeIndex::from(3)],
4778 external_shift: vec![(EdgeIndex::from(5), -1)],
4779 vertex_set: VertexSet::dummy(),
4780 },
4782 ];
4783
4784 for expected_esurface in expected_esurfaces {
4785 assert!(cross_section_cuts.contains(&expected_esurface));
4786 }
4787 }
4788
4789 #[test]
4790 fn test_from_cut_left_box() {
4791 let mut hedge_graph_builder = HedgeGraphBuilder::new();
4792 let nodes = (0..4)
4793 .map(|_| hedge_graph_builder.add_node(()))
4794 .collect_vec();
4795
4796 hedge_graph_builder.add_edge(nodes[0], nodes[1], (), Orientation::Undirected);
4797 hedge_graph_builder.add_edge(nodes[1], nodes[2], (), Orientation::Undirected);
4798 hedge_graph_builder.add_edge(nodes[2], nodes[3], (), Orientation::Undirected);
4799 hedge_graph_builder.add_edge(nodes[3], nodes[0], (), Orientation::Undirected);
4800
4801 hedge_graph_builder.add_external_edge(
4802 nodes[0],
4803 (),
4804 Orientation::Undirected,
4805 Flow::Sink,
4806 );
4807 hedge_graph_builder.add_external_edge(
4808 nodes[1],
4809 (),
4810 Orientation::Undirected,
4811 Flow::Source,
4812 );
4813 hedge_graph_builder.add_external_edge(
4814 nodes[2],
4815 (),
4816 Orientation::Undirected,
4817 Flow::Source,
4818 );
4819 hedge_graph_builder.add_external_edge(
4820 nodes[3],
4821 (),
4822 Orientation::Undirected,
4823 Flow::Sink,
4824 );
4825
4826 let box_graph: HedgeGraph<(), (), ()> =
4827 hedge_graph_builder.build::<NodeStorageVec<()>>();
4828
4829 let node_0 = box_graph.iter_crown(nodes[0]).into();
4830 let node_2 = box_graph.iter_crown(nodes[2]).into();
4831
4832 let cuts = box_graph.all_cuts(node_0, node_2);
4833 assert_eq!(cuts.len(), 4);
4834
4835 let cross_section_cuts = cuts
4836 .into_iter()
4837 .map(|(node_l, cut, node_r)| CrossSectionCut {
4838 cut,
4839 left: node_l,
4840 right: node_r,
4841 })
4842 .map(|cut| Esurface::new_from_cut_left(&box_graph, &cut, None))
4843 .collect_vec();
4844
4845 let expected_esurfaces = vec![
4846 Esurface {
4847 energies: vec![EdgeIndex::from(0), EdgeIndex::from(3)],
4848 external_shift: vec![(EdgeIndex::from(4), -1)],
4849 vertex_set: VertexSet::dummy(),
4850 },
4852 Esurface {
4853 energies: vec![EdgeIndex::from(0), EdgeIndex::from(2)],
4854 external_shift: vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(7), -1)],
4855 vertex_set: VertexSet::dummy(),
4856 },
4858 Esurface {
4859 energies: vec![EdgeIndex::from(1), EdgeIndex::from(3)],
4860 external_shift: vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(5), 1)],
4861 vertex_set: VertexSet::dummy(),
4862 },
4864 Esurface {
4865 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
4866 external_shift: vec![
4867 (EdgeIndex::from(4), -1),
4868 (EdgeIndex::from(5), 1),
4869 (EdgeIndex::from(7), -1),
4870 ],
4871 vertex_set: VertexSet::dummy(),
4872 },
4874 ];
4875
4876 for expected_esurface in expected_esurfaces {
4877 assert!(cross_section_cuts.contains(&expected_esurface));
4878 }
4879 }
4880 }
4881}