1use crate::cff::cff_graph::VertexSet;
2use crate::utils::{cut_energy, external_energy_atom_from_index, ose_atom_from_index};
3use bincode_trait_derive::{Decode, Encode};
4
5use derive_more::{From, Into};
6use itertools::Itertools;
7use linnet::half_edge::involution::EdgeIndex;
8use serde::{Deserialize, Serialize};
9use symbolica::atom::Atom;
10use symbolica::parse;
11use tracing::warn;
12use typed_index_collections::TiVec;
13
14use crate::graph::LoopMomentumBasis;
15use crate::momentum::SignOrZero;
16
17use super::esurface::Esurface;
18use super::esurface::ExternalShift;
19
20#[derive(
21 From, Into, Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encode, Decode, Hash,
22)]
23pub struct HsurfaceID(usize);
24pub type HsurfaceCollection = TiVec<HsurfaceID, Hsurface>;
25pub type HsurfaceCache<T> = TiVec<HsurfaceID, T>;
26
27#[derive(Serialize, Deserialize, Debug, Clone)]
28pub struct Hsurface {
30 pub positive_energies: Vec<EdgeIndex>,
31 pub negative_energies: Vec<EdgeIndex>,
32 pub external_shift: ExternalShift,
33 pub vertex_set: VertexSet,
34}
35
36impl PartialEq for Hsurface {
37 fn eq(&self, other: &Self) -> bool {
38 self.positive_energies == other.positive_energies
39 && self.negative_energies == other.negative_energies
40 }
41}
42
43impl Hsurface {
44 pub(crate) fn to_atom(&self, cut_edges: &[EdgeIndex]) -> Atom {
45 self.to_atom_impl(cut_edges, external_energy_atom_from_index)
46 }
47
48 pub(crate) fn to_atom_in_lmb(&self, cut_edges: &[EdgeIndex], lmb: &LoopMomentumBasis) -> Atom {
49 self.to_atom_impl(cut_edges, |edge| {
50 lmb.edge_signatures[edge].external.iter_enumerated().fold(
51 Atom::Zero,
52 |sum, (external_index, sign)| {
53 let atom = external_energy_atom_from_index(lmb.ext_edges[external_index]);
54 match sign {
55 SignOrZero::Zero => sum,
56 SignOrZero::Plus => sum + atom,
57 SignOrZero::Minus => sum - atom,
58 }
59 },
60 )
61 })
62 }
63
64 fn to_atom_impl(
65 &self,
66 cut_edges: &[EdgeIndex],
67 external_shift_atom: impl Fn(EdgeIndex) -> Atom,
68 ) -> Atom {
69 let (symbolic_positive_energies, symbolic_negative_energies) =
70 [&self.positive_energies, &self.negative_energies]
71 .iter()
72 .map(|energies| {
73 energies
74 .iter()
75 .map(|i| {
76 if cut_edges.contains(i) {
77 cut_energy(*i)
78 } else {
79 ose_atom_from_index(*i)
80 }
81 })
82 .collect_vec()
83 })
84 .collect_tuple()
85 .unwrap_or_else(|| unreachable!());
86
87 let symbolic_shift = self
88 .external_shift
89 .iter()
90 .fold(Atom::new(), |sum, (i, sign)| {
91 Atom::num(*sign) * external_shift_atom(*i) + &sum
92 });
93
94 let symbolic_sum_positive_energies = symbolic_positive_energies
95 .iter()
96 .fold(Atom::new(), |sum, e| sum + e);
97
98 let symbolic_sum_negative_energies = symbolic_negative_energies
99 .iter()
100 .fold(Atom::new(), |sum, e| sum + e);
101
102 symbolic_sum_positive_energies - &symbolic_sum_negative_energies + &symbolic_shift
103 }
104
105 #[allow(dead_code)]
106 pub(crate) fn equality_under_energy_conservation(
107 &self,
108 other: &Esurface,
109 constraints: &[&Esurface],
110 ) -> Option<bool> {
111 if !self.external_shift.is_empty() {
112 warn!("this is not handled yet");
113 return None;
114 }
115 constraints
116 .iter()
117 .find(|esurface| {
118 self.negative_energies
119 .iter()
120 .all(|index| esurface.energies.contains(index))
121 })
122 .map(|constraint| {
123 let energies_to_be_added = constraint
124 .energies
125 .iter()
126 .filter(|index| !self.negative_energies.contains(index));
127
128 let mut new_positive_energies = self.positive_energies.clone();
129 new_positive_energies.extend(energies_to_be_added);
130 new_positive_energies.sort();
131
132 let new_esurface = Esurface {
133 energies: new_positive_energies,
134 external_shift: constraint.external_shift.clone(),
135 vertex_set: VertexSet::dummy(),
136 };
137
138 other == &new_esurface
139 })
140 }
141
142 pub fn equality_by_try_convert(&self, other: &Esurface) -> bool {
143 let negative_as_external_shift = self
144 .negative_energies
145 .iter()
146 .map(|e| (*e, -1))
147 .collect_vec();
148
149 let self_as_esurface = Esurface {
150 energies: self.positive_energies.clone(),
151 external_shift: negative_as_external_shift,
152 vertex_set: VertexSet::dummy(),
153 };
154
155 self_as_esurface == *other
156 }
157}
158
159impl From<HsurfaceID> for Atom {
160 fn from(value: HsurfaceID) -> Self {
161 parse!(&format!("H({})", Into::<usize>::into(value)))
162 }
163}
164
165#[cfg(test)]
166mod tests {
167
168 use linnet::half_edge::involution::EdgeIndex;
169 use linnet::half_edge::subgraph::{SuBitGraph, SubSetLike};
170 use symbolica::atom::{Atom, AtomCore};
171
172 use symbolica::parse;
173
174 use crate::cff::{cff_graph::VertexSet, esurface::Esurface};
175 use crate::graph::LoopMomentumBasis;
176 use crate::momentum::signature::LoopExtSignature;
177 use crate::utils::{external_energy_atom_from_index, test_utils::dummy_hedge_graph};
178
179 use super::Hsurface;
180
181 #[test]
182 fn test_equality_under_energy_conservation() {
183 let constraint = Esurface {
184 energies: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
185 external_shift: vec![(EdgeIndex::from(0), -1)],
186 vertex_set: VertexSet::dummy(),
187 };
188
189 let hsurface = Hsurface {
190 positive_energies: vec![EdgeIndex::from(3), EdgeIndex::from(5)],
191 negative_energies: vec![EdgeIndex::from(2)],
192 external_shift: vec![],
193 vertex_set: VertexSet::dummy(),
194 };
195
196 let other = Esurface {
197 energies: vec![EdgeIndex::from(1), EdgeIndex::from(3), EdgeIndex::from(5)],
198 external_shift: vec![(EdgeIndex::from(0), -1)],
199 vertex_set: VertexSet::dummy(),
200 };
201
202 assert!(
203 hsurface
204 .equality_under_energy_conservation(&other, &[&constraint])
205 .unwrap()
206 );
207 }
208
209 #[test]
210 fn to_atom_in_lmb_uses_canonical_external_edges_not_carrier_edges() {
211 let dummy_graph = dummy_hedge_graph(9);
212 let mut edge_signatures = dummy_graph
213 .new_edgevec_from_iter(
214 (0..9).map(|_| LoopExtSignature::from((Vec::<isize>::new(), vec![0, 0]))),
215 )
216 .unwrap();
217 edge_signatures[EdgeIndex::from(8)] =
218 LoopExtSignature::from((Vec::<isize>::new(), vec![0, 1]));
219 let lmb = LoopMomentumBasis {
220 tree: SuBitGraph::empty(0),
221 loop_edges: vec![].into(),
222 ext_edges: vec![EdgeIndex::from(2), EdgeIndex::from(6)].into(),
223 edge_signatures,
224 };
225 let hsurface = Hsurface {
226 positive_energies: vec![],
227 negative_energies: vec![],
228 external_shift: vec![(EdgeIndex::from(8), -1)],
229 vertex_set: VertexSet::dummy(),
230 };
231
232 let atom = hsurface.to_atom_in_lmb(&[], &lmb).expand();
233 let expected =
234 (Atom::num(-1) * external_energy_atom_from_index(EdgeIndex::from(6))).expand();
235
236 assert_eq!(atom.to_canonical_string(), expected.to_canonical_string());
237 }
238
239 mod failing {
240 use super::*;
241
242 #[test]
243 fn test_to_atom() {
244 let external_shift = vec![(EdgeIndex::from(4), -1), (EdgeIndex::from(5), 1)];
245 let h_surface = Hsurface {
246 positive_energies: vec![EdgeIndex::from(0), EdgeIndex::from(1)],
247 negative_energies: vec![EdgeIndex::from(2), EdgeIndex::from(3)],
248 external_shift,
249 vertex_set: VertexSet::dummy(),
250 };
251
252 let h_surface_atom = h_surface.to_atom(&[]);
253 let expected_atom = parse!(
254 "Q(0, cind(0)) + Q(1, cind(0)) - Q(2, cind(0)) - Q(3, cind(0)) - P(4, cind(0)) + P(5, cind(0))"
255 );
256 let diff = h_surface_atom - &expected_atom;
257 let diff = diff.expand();
258 assert_eq!(diff, Atom::new());
259 }
260 }
261}