1use crate::cff::VertexSet;
2use crate::graph::LoopMomentumBasis;
3use crate::momentum::sample::{ExternalFourMomenta, ExternalThreeMomenta};
4use crate::utils::{F, cut_energy, external_energy_atom_from_index, ose_atom_from_index};
5use bincode_trait_derive::{Decode, Encode};
6use itertools::Itertools;
7use linnet::half_edge::involution::{EdgeIndex, EdgeVec};
8use rug::Rational;
9use serde::{Deserialize, Serialize};
10use spenso::algebra::complex::Complex;
11use symbolica::atom::Atom;
12use tracing::warn;
13use typed_index_collections::TiVec;
14
15use super::esurface::Esurface;
16use super::esurface::ExternalShift;
17pub use super::surface::HsurfaceID;
18
19pub type HsurfaceCollection = TiVec<HsurfaceID, Hsurface>;
20pub type HsurfaceCache<T> = TiVec<HsurfaceID, T>;
21
22#[derive(Serialize, Deserialize, Debug, Clone, Encode, Decode)]
23pub struct Hsurface {
25 pub positive_energies: Vec<EdgeIndex>,
26 pub negative_energies: Vec<EdgeIndex>,
27 pub external_shift: ExternalShift,
28 pub vertex_set: VertexSet,
29}
30
31impl PartialEq for Hsurface {
32 fn eq(&self, other: &Self) -> bool {
33 self.positive_energies == other.positive_energies
34 && self.negative_energies == other.negative_energies
35 && self.external_shift == other.external_shift
36 }
37}
38
39impl Eq for Hsurface {}
40
41impl Hsurface {
42 pub(crate) fn to_atom(&self, cut_edges: &[EdgeIndex]) -> Atom {
43 let (symbolic_positive_energies, symbolic_negative_energies) =
44 [&self.positive_energies, &self.negative_energies]
45 .iter()
46 .map(|energies| {
47 energies
48 .iter()
49 .map(|i| {
50 if cut_edges.contains(i) {
51 cut_energy(*i)
52 } else {
53 ose_atom_from_index(*i)
54 }
55 })
56 .collect_vec()
57 })
58 .collect_tuple()
59 .unwrap_or_else(|| unreachable!());
60
61 let symbolic_shift = self
62 .external_shift
63 .iter()
64 .fold(Atom::new(), |sum, (i, sign)| {
65 Atom::num(*sign) * external_energy_atom_from_index(*i) + &sum
66 });
67
68 let symbolic_sum_positive_energies = symbolic_positive_energies
69 .iter()
70 .fold(Atom::new(), |sum, e| sum + e);
71
72 let symbolic_sum_negative_energies = symbolic_negative_energies
73 .iter()
74 .fold(Atom::new(), |sum, e| sum + e);
75
76 symbolic_sum_positive_energies - &symbolic_sum_negative_energies + &symbolic_shift
77 }
78
79 pub(crate) fn vanishes_identically(
87 &self,
88 lmb: &LoopMomentumBasis,
89 masses: &EdgeVec<Option<Complex<F<f64>>>>,
90 externals: &ExternalFourMomenta<F<f64>>,
91 ) -> eyre::Result<bool> {
92 let spatial_externals: ExternalThreeMomenta<F<f64>> =
93 externals.iter().map(|momentum| momentum.spatial).collect();
94 let mut unpaired = self.negative_energies.clone();
95 for &positive in &self.positive_energies {
96 let mut partner = None;
97 for (position, &negative) in unpaired.iter().enumerate() {
98 if masses[positive]
100 .is_some_and(|m| masses[negative].is_some_and(|n| m == n || m == -n))
101 && lmb.edge_signatures[positive].spatial_equality_up_to_sign(
102 &lmb.edge_signatures[negative],
103 &spatial_externals,
104 )?
105 {
106 partner = Some(position);
107 break;
108 }
109 }
110 let Some(position) = partner else {
111 return Ok(false);
112 };
113 unpaired.swap_remove(position);
114 }
115 if !unpaired.is_empty() {
116 return Ok(false);
117 }
118 let mut shift = Rational::new();
119 for (edge, sign) in &self.external_shift {
120 let signature = lmb.edge_signatures[*edge].external.to_momtrop_format();
121 for (coefficient, momentum) in signature.into_iter().zip(externals) {
122 if coefficient != 0 {
123 let energy =
124 Rational::from_f64(momentum.temporal.value.0).ok_or_else(|| {
125 eyre::eyre!("nonfinite external energy {}", momentum.temporal.value)
126 })?;
127 shift += energy * (sign * coefficient as i64);
128 }
129 }
130 }
131 Ok(shift == 0)
132 }
133
134 #[allow(dead_code)]
135 pub(crate) fn equality_under_energy_conservation(
136 &self,
137 other: &Esurface,
138 constraints: &[&Esurface],
139 ) -> Option<bool> {
140 if !self.external_shift.is_empty() {
141 warn!("this is not handled yet");
142 return None;
143 }
144 constraints
145 .iter()
146 .find(|esurface| {
147 self.negative_energies
148 .iter()
149 .all(|index| esurface.energies.contains(index))
150 })
151 .map(|constraint| {
152 let energies_to_be_added = constraint
153 .energies
154 .iter()
155 .filter(|index| !self.negative_energies.contains(index));
156
157 let mut new_positive_energies = self.positive_energies.clone();
158 new_positive_energies.extend(energies_to_be_added);
159 new_positive_energies.sort();
160
161 let new_esurface = Esurface {
162 energies: new_positive_energies,
163 external_shift: constraint.external_shift.clone(),
164 vertex_set: VertexSet::dummy(),
165 };
166
167 other == &new_esurface
168 })
169 }
170
171 pub fn equality_by_try_convert(&self, other: &Esurface) -> bool {
172 let negative_as_external_shift = self
173 .negative_energies
174 .iter()
175 .map(|e| (*e, -1))
176 .collect_vec();
177
178 let self_as_esurface = Esurface {
179 energies: self.positive_energies.clone(),
180 external_shift: negative_as_external_shift,
181 vertex_set: VertexSet::dummy(),
182 };
183
184 self_as_esurface == *other
185 }
186}
187
188#[cfg(test)]
189mod tests {
190
191 use linnet::half_edge::involution::EdgeIndex;
192 use symbolica::atom::{Atom, AtomCore};
193
194 use symbolica::parse;
195
196 use crate::cff::{VertexSet, esurface::Esurface};
197
198 use super::Hsurface;
199
200 #[test]
201 fn test_equality_under_energy_conservation() {
202 let constraint = Esurface {
203 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
204 external_shift: vec![(EdgeIndex::from(0), -1)],
205 vertex_set: VertexSet::dummy(),
206 };
207
208 let hsurface = Hsurface {
209 positive_energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
210 negative_energies: vec![EdgeIndex::from(2)],
211 external_shift: vec![],
212 vertex_set: VertexSet::dummy(),
213 };
214
215 let other = Esurface {
216 energies: vec![EdgeIndex::from(1), EdgeIndex::from(3), EdgeIndex::from(5)],
217 external_shift: vec![(EdgeIndex::from(0), -1)],
218 vertex_set: VertexSet::dummy(),
219 };
220
221 assert!(
222 hsurface
223 .equality_under_energy_conservation(&other, &[&constraint])
224 .unwrap()
225 );
226 }
227
228 #[test]
229 fn surfaces_vanish_identically_only_for_paired_energies_without_energy_shift() {
230 use linnet::half_edge::subgraph::{SuBitGraph, SubSetLike};
231 use spenso::algebra::complex::Complex;
232
233 use crate::{
234 graph::LoopMomentumBasis,
235 momentum::{
236 FourMomentum,
237 sample::{ExternalFourMomenta, ExternalIndex},
238 signature::LoopExtSignature,
239 },
240 utils::{F, test_utils::dummy_hedge_graph},
241 };
242
243 let graph = dummy_hedge_graph(5);
246 let edge_signatures = graph
247 .new_edgevec_from_iter(
248 [
249 (vec![1], vec![0, 0]),
250 (vec![1], vec![1, 0]),
251 (vec![0], vec![1, 0]),
252 (vec![0], vec![0, 1]),
253 (vec![1], vec![0, 0]),
254 ]
255 .map(LoopExtSignature::from),
256 )
257 .unwrap();
258 let lmb = LoopMomentumBasis {
259 tree: SuBitGraph::empty(0),
260 loop_edges: vec![EdgeIndex(0)].into(),
261 ext_edges: vec![EdgeIndex(2), EdgeIndex(3)].into(),
262 edge_signatures,
263 };
264 let surface = |positive: &[usize], negative: &[usize], external_shift| Hsurface {
265 positive_energies: positive.iter().copied().map(EdgeIndex).collect(),
266 negative_energies: negative.iter().copied().map(EdgeIndex).collect(),
267 external_shift,
268 vertex_set: VertexSet::dummy(),
269 };
270 let masses = |values: [Option<f64>; 5]| {
271 graph
272 .new_edgevec_from_iter(values.map(|mass| mass.map(|m| Complex::new_re(F(m)))))
273 .unwrap()
274 };
275 let externals = |energy: f64, py: f64| -> ExternalFourMomenta<F<f64>> {
276 let p = FourMomentum::from_args(F(energy), F(0.0), F(py), F(0.0));
277 [p, p].into_iter().collect()
278 };
279 let equal = masses([Some(1.0), Some(1.0), None, None, Some(1.0)]);
280 let rest = externals(0.0, 0.0);
281
282 let cycle = surface(&[4], &[0], vec![]);
284 assert!(cycle.vanishes_identically(&lmb, &equal, &rest).unwrap());
285 let split = masses([Some(1.0), Some(1.0), None, None, Some(2.0)]);
286 assert!(!cycle.vanishes_identically(&lmb, &split, &rest).unwrap());
287 for (mass, vanishes) in [(-1.0, true), ((-1.0_f64).next_up(), false), (-2.0, false)] {
288 let signed = masses([Some(1.0), Some(1.0), None, None, Some(mass)]);
289 assert_eq!(
290 cycle.vanishes_identically(&lmb, &signed, &rest).unwrap(),
291 vanishes,
292 "mass {mass} paired with 1"
293 );
294 }
295 let mut reversed_lmb = lmb.clone();
297 reversed_lmb.edge_signatures[EdgeIndex(4)] = LoopExtSignature::from((vec![-1], vec![0, 0]));
298 let opposite = masses([Some(1.0), Some(1.0), None, None, Some(-1.0)]);
299 assert!(
300 cycle
301 .vanishes_identically(&reversed_lmb, &opposite, &rest)
302 .unwrap()
303 );
304 for (mass, vanishes) in [
306 (Complex::new(F(1.0), F(2.0)), true),
307 (Complex::new(F(-1.0), F(-2.0)), true),
308 (Complex::new(F(1.0), F(-2.0)), false),
309 ] {
310 let mut complex = equal.clone();
311 complex[EdgeIndex(0)] = Some(Complex::new(F(1.0), F(2.0)));
312 complex[EdgeIndex(4)] = Some(mass);
313 assert_eq!(
314 cycle.vanishes_identically(&lmb, &complex, &rest).unwrap(),
315 vanishes,
316 "mass {mass} paired with 1 + 2i"
317 );
318 }
319 let unknown = masses([Some(1.0), Some(1.0), None, None, None]);
320 assert!(!cycle.vanishes_identically(&lmb, &unknown, &rest).unwrap());
321 let unpaired = surface(&[0, 1], &[4], vec![]);
322 assert!(!unpaired.vanishes_identically(&lmb, &equal, &rest).unwrap());
323
324 let bubble = surface(&[1], &[0], vec![(EdgeIndex(2), -1)]);
327 assert!(bubble.vanishes_identically(&lmb, &equal, &rest).unwrap());
328 for moving in [externals(0.0, 0.5), externals(0.5, 0.0)] {
329 assert!(!bubble.vanishes_identically(&lmb, &equal, &moving).unwrap());
330 }
331 let heavy = masses([Some(1.0), Some(2.0), None, None, Some(1.0)]);
332 assert!(!bubble.vanishes_identically(&lmb, &heavy, &rest).unwrap());
333 let mut nonfinite = rest.clone();
334 nonfinite[ExternalIndex(0)].temporal.value = F(f64::NAN);
335 assert!(
336 bubble
337 .vanishes_identically(&lmb, &equal, &nonfinite)
338 .is_err()
339 );
340 }
341
342 #[test]
343 fn cache_equality_includes_the_external_shift_but_not_the_shore() {
344 let base = Hsurface {
345 positive_energies: vec![EdgeIndex::from(0)],
346 negative_energies: vec![EdgeIndex::from(1)],
347 external_shift: vec![(EdgeIndex::from(2), 1)],
348 vertex_set: VertexSet::from_usize(0),
349 };
350 let different_shore = Hsurface {
351 vertex_set: VertexSet::from_usize(1),
352 ..base.clone()
353 };
354 let different_shift = Hsurface {
355 external_shift: vec![(EdgeIndex::from(3), 1)],
356 ..base.clone()
357 };
358
359 assert_eq!(base, different_shore);
360 assert_ne!(base, different_shift);
361 assert_ne!(
362 base.to_atom(&[]).to_canonical_string(),
363 different_shift.to_atom(&[]).to_canonical_string()
364 );
365 }
366
367 #[test]
368 fn test_to_atom() {
369 let external_shift = vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(5), 1)];
370 let h_surface = Hsurface {
371 positive_energies: vec![EdgeIndex::from(0), EdgeIndex::from(1)],
372 negative_energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
373 external_shift,
374 vertex_set: VertexSet::dummy(),
375 };
376
377 let h_surface_atom = h_surface.to_atom(&[]);
378 let expected_atom = parse!(
379 "OSE(0) + OSE(1) - OSE(2) - OSE(3) - Q(4, spenso::cind(0)) + Q(5, spenso::cind(0))"
380 );
381 let diff = h_surface_atom - &expected_atom;
382 let diff = diff.expand();
383 assert_eq!(diff, Atom::new());
384 }
385}