Skip to main content

gammalooprs/momentum/
sample.rs

1use std::fmt::Display;
2
3use crate::graph::{Graph, LMBext, LmbIndex, LoopMomentumBasis};
4use crate::integrands::process::evaluators::SingleOrAllOrientations;
5use crate::momentum::{FourMomentum, Polarization, Rotatable, Rotation, SignOrZero, ThreeMomentum};
6
7use crate::momentum::signature::LoopSignature;
8use crate::utils::hyperdual_utils::new_constant;
9use crate::utils::{F, FloatLike, Length, PrecisionUpgradable};
10use crate::{DependentMomentaConstructor, define_index, settings::runtime::kinematic::Externals};
11use bincode_trait_derive::{Decode, Encode};
12use color_eyre::Result;
13use derive_more::{From, Into};
14use eyre::eyre;
15use linnet::half_edge::HedgeGraph;
16use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Orientation};
17use linnet::half_edge::nodestore::NodeStorageOps;
18use linnet::half_edge::subgraph::subset::SubSet;
19use linnet::half_edge::subgraph::{
20    Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps,
21};
22use linnet::half_edge::typed_vec::IndexLike;
23use serde::{Deserialize, Serialize};
24use std::ops::{Add, Index, IndexMut, Sub};
25use symbolica::domains::dual::HyperDual;
26use tabled::settings::Style;
27
28use typed_index_collections::TiVec;
29
30#[derive(
31    From,
32    Into,
33    Copy,
34    Clone,
35    Hash,
36    Eq,
37    Ord,
38    PartialEq,
39    PartialOrd,
40    bincode::Encode,
41    bincode::Decode,
42    Debug,
43    Serialize,
44    Deserialize,
45)]
46pub struct LoopIndex(pub usize);
47
48// #[derive(
49//     From,
50//     Into,
51//     Copy,
52//     Clone,
53//     Hash,
54//     Eq,
55//     Ord,
56//     PartialEq,
57//     PartialOrd,
58//     Encode,
59//     Decode,
60//     Debug,
61//     Serialize,
62//     Deserialize,
63// )]
64// pub struct ExternalIndex(pub usize);
65
66define_index!(
67    pub struct ExternalIndex;
68);
69
70// define_indexed_vec!(
71//     EdgeIndex;
72
73//     pub struct ExternalThr;
74// );
75
76impl Display for LoopIndex {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.0)
79    }
80}
81
82impl Display for ExternalIndex {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", self.0)
85    }
86}
87
88#[derive(From, Into, Serialize, Deserialize, Clone, PartialEq, Debug, Encode, Decode)]
89pub struct LoopMomenta<T>(pub Vec<ThreeMomentum<T>>);
90
91impl<T: Display> Display for LoopMomenta<T> {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        let mut table_builder = tabled::builder::Builder::new();
94        for (i, mom) in self.0.iter().enumerate() {
95            let mut row = Vec::new();
96            row.push(i.to_string());
97            for m in mom {
98                row.push(m.to_string());
99            }
100            table_builder.push_record(row);
101        }
102
103        write!(f, "{}", table_builder.build().with(Style::modern_rounded()))
104    }
105}
106
107impl<T> Length for LoopMomenta<T> {
108    fn is_empty(&self) -> bool {
109        self.0.is_empty()
110    }
111
112    fn len(&self) -> usize {
113        self.0.len()
114    }
115}
116
117pub type Subspace<'a> = Option<&'a [LoopIndex]>; // None means full space
118
119#[derive(Clone, Debug, bincode::Encode, bincode::Decode)]
120pub struct SubspaceData {
121    subgraph: InternalSubGraph,
122    #[bincode(with_serde)]
123    complement_subgraph: SuBitGraph,
124    lmb: LmbIndex,
125    lmb_indices: Vec<LoopIndex>,
126}
127
128impl SubspaceData {
129    pub(crate) fn is_mergable_with(&self, other: &Self) -> bool {
130        self.lmb == other.lmb
131            && self
132                .lmb_indices
133                .iter()
134                .all(|idx| !other.lmb_indices.contains(idx))
135            && other
136                .lmb_indices
137                .iter()
138                .all(|idx| !self.lmb_indices.contains(idx))
139    }
140
141    fn cleaned_filter_pessimist<E, V, H, N: NodeStorageOps<NodeData = V>>(
142        mut filter: SuBitGraph,
143        graph: &HedgeGraph<E, V, H, N>,
144    ) -> InternalSubGraph {
145        let mut to_remove = SuBitGraph::empty(filter.size());
146
147        for i in filter.included_iter() {
148            if !filter.includes(&graph.inv(i)) {
149                to_remove.add(i);
150            }
151        }
152        filter.subtract_with(&to_remove);
153
154        InternalSubGraph {
155            filter,
156            loopcount: None,
157        }
158    }
159
160    pub(crate) fn new_with_user_selected_lmb(
161        subgraph: SuBitGraph,
162        lmb: LmbIndex,
163        graph: &Graph,
164        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
165    ) -> Result<Self> {
166        let bridges = graph.bridges_of(&subgraph);
167
168        let mut subgraph = Self::cleaned_filter_pessimist(subgraph.subtract(&bridges), graph);
169        subgraph.set_loopcount(graph);
170
171        let edges_in_subgraph = graph
172            .iter_edges_of(&subgraph.filter)
173            .map(|ed| ed.1)
174            .collect::<Vec<_>>();
175        let complement_subgraph = graph.full_filter().subtract(&subgraph.filter);
176
177        let parent_lmb = &all_lmbs[lmb];
178        let compatible_sub_lmb =
179            graph.try_compatible_sub_lmb(&subgraph, graph.full_crown(&subgraph), parent_lmb)?;
180        let mut lmb_indices = compatible_sub_lmb
181            .loop_edges
182            .iter()
183            .map(|edge_index| {
184                parent_lmb
185                    .loop_edges
186                    .iter_enumerated()
187                    .find_map(|(loop_index, parent_edge)| {
188                        (parent_edge == edge_index).then_some(loop_index)
189                    })
190                    .ok_or_else(|| {
191                        eyre!(
192                            "Compatible subgraph LMB edge {:?} is not a defining edge of parent LMB {:?}",
193                            edge_index,
194                            parent_lmb.loop_edges,
195                        )
196                    })
197            })
198            .collect::<Result<Vec<_>>>()?;
199        lmb_indices.sort();
200
201        if lmb_indices.len() != subgraph.loopcount.unwrap()
202            || compatible_sub_lmb.loop_edges.len() != subgraph.loopcount.unwrap()
203        {
204            return Err(color_eyre::eyre::eyre!(
205                "Provided loop momentum basis is not topologically compatible with subgraph, lmb_indices: {:?}, lmb_edges: {:?}, compatible_sub_lmb_edges: {:?}, subgraph loopcount: {}, edges_in_subgraph: {:?}",
206                lmb_indices,
207                parent_lmb.loop_edges,
208                compatible_sub_lmb.loop_edges,
209                subgraph.loopcount.unwrap(),
210                edges_in_subgraph,
211            ));
212        }
213
214        Ok(Self {
215            subgraph,
216            complement_subgraph,
217            lmb,
218            lmb_indices,
219        })
220    }
221
222    /// this function chooses the lmb automatically based on the subgraph
223    #[allow(dead_code)]
224    pub(crate) fn new(
225        subgraph: SuBitGraph,
226        graph: &Graph,
227        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
228    ) -> Result<Self> {
229        let mut errors = Vec::new();
230        for (lmb_index, _) in all_lmbs.iter_enumerated() {
231            match Self::new_with_user_selected_lmb(subgraph.clone(), lmb_index, graph, all_lmbs) {
232                Ok(subspace) => return Ok(subspace),
233                Err(error) => errors.push(format!("LMB {}: {error:#}", usize::from(lmb_index))),
234            }
235        }
236
237        Err(eyre!(
238            "No topology-compatible loop momentum basis found for subgraph:\n{}",
239            errors.join("\n")
240        ))
241    }
242    pub(crate) fn get_lmb<'a>(
243        &self,
244        all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
245    ) -> &'a LoopMomentumBasis {
246        &all_lmbs[self.lmb]
247    }
248
249    pub(crate) fn does_not_contain<'a>(
250        &'a self,
251        edges: &'a [EdgeIndex],
252        graph: &'a Graph,
253    ) -> impl Iterator<Item = EdgeIndex> + 'a {
254        graph
255            .iter_edges_of(&self.complement_subgraph)
256            .map(|ed| ed.1)
257            .filter(|e| edges.contains(e))
258    }
259
260    pub(crate) fn contains<'a>(
261        &'a self,
262        edges: &'a [EdgeIndex],
263        graph: &'a Graph,
264    ) -> impl Iterator<Item = EdgeIndex> + 'a {
265        graph
266            .iter_edges_of(&self.subgraph)
267            .map(|ed| ed.1)
268            .filter(|e| edges.contains(e))
269    }
270
271    pub(crate) fn project_loop_signature<'a>(
272        &'a self,
273        signature: &'a LoopSignature,
274    ) -> impl Iterator<Item = SignOrZero> + 'a {
275        signature.iter_enumerated().map(|(loop_index, sign)| {
276            if self.lmb_indices.contains(&loop_index) {
277                *sign
278            } else {
279                SignOrZero::Zero
280            }
281        })
282    }
283
284    pub(crate) fn project_loop_signature_filtered<'a>(
285        &'a self,
286        signature: &'a LoopSignature,
287    ) -> impl Iterator<Item = SignOrZero> + 'a {
288        signature
289            .iter_enumerated()
290            .filter_map(move |(loop_index, sign)| {
291                if self.lmb_indices.contains(&loop_index) {
292                    Some(*sign)
293                } else {
294                    None
295                }
296            })
297    }
298
299    pub(crate) fn project_complement_loop_signature<'a>(
300        &'a self,
301        signature: &'a LoopSignature,
302    ) -> impl Iterator<Item = SignOrZero> + 'a {
303        signature.iter_enumerated().map(|(loop_index, sign)| {
304            if !self.lmb_indices.contains(&loop_index) {
305                *sign
306            } else {
307                SignOrZero::Zero
308            }
309        })
310    }
311
312    pub(crate) fn iter_lmb_indices<'a>(&'a self) -> impl Iterator<Item = LoopIndex> + 'a {
313        self.lmb_indices.iter().copied()
314    }
315
316    pub(crate) fn iter_basis_edges<'a>(
317        &'a self,
318        all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
319    ) -> impl Iterator<Item = EdgeIndex> + 'a {
320        self.lmb_indices
321            .iter()
322            .map(|&loop_index| self.get_lmb(all_lmbs).loop_edges[loop_index])
323    }
324
325    pub(crate) fn contains_loop_index(&self, loop_index: LoopIndex) -> bool {
326        self.lmb_indices.contains(&loop_index)
327    }
328
329    pub(crate) fn loopcount(&self) -> usize {
330        self.subgraph.loopcount.unwrap()
331    }
332
333    pub(crate) fn as_subspace_simple(&self) -> Subspace<'_> {
334        Some(self.lmb_indices.as_slice())
335    }
336}
337
338// #[comemo::track]
339// impl<T> LoopMomenta<T> {}
340
341impl<T> LoopMomenta<T> {
342    pub(crate) fn iter(&self) -> std::slice::Iter<'_, ThreeMomentum<T>> {
343        self.0.iter()
344    }
345
346    pub(crate) fn first(&self) -> Option<&ThreeMomentum<T>> {
347        self.0.first()
348    }
349
350    pub(crate) fn iter_enumerated(&self) -> impl Iterator<Item = (LoopIndex, &ThreeMomentum<T>)> {
351        self.0.iter().enumerate().map(|(i, m)| (LoopIndex(i), m))
352    }
353}
354
355impl<T> IntoIterator for LoopMomenta<T> {
356    type Item = ThreeMomentum<T>;
357    type IntoIter = std::vec::IntoIter<ThreeMomentum<T>>;
358
359    fn into_iter(self) -> Self::IntoIter {
360        self.0.into_iter()
361    }
362}
363
364impl<T> FromIterator<ThreeMomentum<T>> for LoopMomenta<T> {
365    fn from_iter<I: IntoIterator<Item = ThreeMomentum<T>>>(iter: I) -> Self {
366        LoopMomenta(iter.into_iter().collect())
367    }
368}
369
370impl<T> Index<LoopIndex> for LoopMomenta<T> {
371    type Output = ThreeMomentum<T>;
372
373    fn index(&self, index: LoopIndex) -> &Self::Output {
374        &self.0[index.0]
375    }
376}
377
378impl<T> IndexMut<LoopIndex> for LoopMomenta<T> {
379    fn index_mut(&mut self, index: LoopIndex) -> &mut Self::Output {
380        &mut self.0[index.0]
381    }
382}
383
384impl<T: FloatLike> LoopMomenta<F<T>> {
385    pub(crate) fn hyper_radius_squared(&self, subspace: Subspace) -> F<T> {
386        let zero = self.0[0].px.zero();
387        match subspace {
388            None => self.iter().fold(zero, |acc, x| acc + x.norm_squared()),
389            Some(subspace) => subspace
390                .iter()
391                .fold(zero, |acc, &i| acc + self[i].norm_squared()),
392        }
393    }
394
395    pub(crate) fn rescale(&self, factor: &F<T>, subspace: Subspace) -> Self {
396        match subspace {
397            None => LoopMomenta::from_iter(self.iter().map(|k| k * factor)),
398            // this branch is wrong
399            Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
400                if subspace.contains(&i) {
401                    k * factor
402                } else {
403                    k.clone()
404                }
405            })),
406        }
407    }
408
409    pub(crate) fn rescale_with_hyper_dual(
410        &self,
411        factor: &HyperDual<F<T>>,
412        subspace: Subspace,
413    ) -> LoopMomenta<HyperDual<F<T>>> {
414        match subspace {
415            None => LoopMomenta::from_iter(self.iter().map(|k| {
416                ThreeMomentum::new(
417                    new_constant(factor, &k.px) * factor,
418                    new_constant(factor, &k.py) * factor,
419                    new_constant(factor, &k.pz) * factor,
420                )
421            })),
422            Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
423                if subspace.contains(&i) {
424                    ThreeMomentum::new(
425                        new_constant(factor, &k.px) * factor,
426                        new_constant(factor, &k.py) * factor,
427                        new_constant(factor, &k.pz) * factor,
428                    )
429                } else {
430                    ThreeMomentum::new(
431                        new_constant(factor, &k.px),
432                        new_constant(factor, &k.py),
433                        new_constant(factor, &k.pz),
434                    )
435                }
436            })),
437        }
438    }
439
440    pub(crate) fn rotate(&self, rotation: &Rotation) -> Self {
441        LoopMomenta::from_iter(self.iter().map(|k| k.rotate(rotation)))
442    }
443
444    pub(crate) fn lmb_transform(
445        &self,
446        from: &LoopMomentumBasis,
447        to: &LoopMomentumBasis,
448        externals: &ExternalThreeMomenta<F<T>>,
449    ) -> Self {
450        LoopMomenta::from_iter(
451            to.loop_edges
452                .iter()
453                .map(|e_id| from.edge_signatures[*e_id].compute_momentum(self, externals)),
454        )
455    }
456}
457
458impl<T: FloatLike> LoopMomenta<HyperDual<F<T>>> {
459    pub fn lmb_transform(
460        &self,
461        from: &LoopMomentumBasis,
462        to: &LoopMomentumBasis,
463        externals: &ExternalThreeMomenta<HyperDual<F<T>>>,
464    ) -> Self {
465        LoopMomenta::from_iter(to.loop_edges.iter().map(|e_id| {
466            from.edge_signatures[*e_id]
467                .try_compute_momentum(&self.0, &externals.raw)
468                .unwrap()
469        }))
470    }
471
472    pub fn rescale(&self, factor: &HyperDual<F<T>>, subspace: Subspace) -> Self {
473        match subspace {
474            None => LoopMomenta::from_iter(self.iter().map(|k| k * factor)),
475            // this branch is wrong
476            Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
477                if subspace.contains(&i) {
478                    k * factor
479                } else {
480                    k.clone()
481                }
482            })),
483        }
484    }
485}
486
487impl LoopMomenta<F<f64>> {
488    pub(crate) fn cast<T: FloatLike>(&self) -> LoopMomenta<F<T>> {
489        LoopMomenta::from_iter(self.iter().map(|m| m.map(&|x| F::from_ff64(x))))
490    }
491}
492
493impl<T: FloatLike> Sub<&LoopMomenta<F<T>>> for &LoopMomenta<F<T>> {
494    type Output = LoopMomenta<F<T>>;
495
496    fn sub(self, rhs: &LoopMomenta<F<T>>) -> Self::Output {
497        LoopMomenta::from_iter(self.iter().zip(rhs.iter()).map(|(l, r)| l - r))
498    }
499}
500
501impl<T: FloatLike> Add<&LoopMomenta<F<T>>> for &LoopMomenta<F<T>> {
502    type Output = LoopMomenta<F<T>>;
503
504    fn add(self, rhs: &LoopMomenta<F<T>>) -> Self::Output {
505        LoopMomenta::from_iter(self.iter().zip(rhs.iter()).map(|(l, r)| l + r))
506    }
507}
508
509// define_indexed_vec!(
510//     MyIdx;
511//     pub struct ExternalMomentumtwo<ThreeMomentum<T>>;
512// );
513pub type ExternalThreeMomenta<T> = TiVec<ExternalIndex, ThreeMomentum<T>>;
514// define_indexed_vec!()
515pub type ExternalFourMomenta<T> = TiVec<ExternalIndex, FourMomentum<T>>;
516// impl<T: Display> Display for ExternalFourMomenta<T> {
517//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518//         let mut table_builder = tabled::builder::Builder::new();
519//         for (i, mom) in self.iter_enumerated() {
520//             let mut row = Vec::new();
521//             row.push(i.to_string());
522//             for m in mom {
523//                 row.push(m.to_string());
524//             }
525//             table_builder.push_record(row);
526//         }
527
528//         write!(f, "{}", table_builder.build().with(Style::modern_rounded()))
529//     }
530// }
531pub type PolarizationVectors<T> = TiVec<ExternalIndex, Polarization<T>>; // should be the same length as #externals
532
533fn extract_external_spatial<T: Clone>(
534    external_four_momenta: &ExternalFourMomenta<T>,
535) -> ExternalThreeMomenta<T> {
536    ExternalThreeMomenta::from_iter(external_four_momenta.iter().map(|fm| fm.spatial.clone()))
537}
538
539#[derive(Debug, Clone)]
540pub struct BareMomentumSample<T: FloatLike> {
541    pub loop_moms: LoopMomenta<F<T>>,
542    pub dual_loop_moms: Option<LoopMomenta<HyperDual<F<T>>>>,
543    pub loop_mom_cache_id: usize,
544    /// Base cache ID for the fundamental loop momentum configuration (before transformations)
545    pub loop_mom_base_cache_id: usize,
546    pub external_moms: ExternalFourMomenta<F<T>>,
547    pub external_mom_cache_id: usize,
548    /// Base cache ID for the fundamental external momentum configuration (before transformations)
549    pub external_mom_base_cache_id: usize,
550    pub jacobian: F<T>,
551    pub orientation: Option<usize>,
552    pub parameterization_branch: Option<usize>,
553}
554
555#[derive(Debug, Clone)]
556pub struct MomentumSample<T: FloatLike> {
557    pub sample: BareMomentumSample<T>,
558    // pub uuid: Uuid,
559}
560
561impl<T: FloatLike> Display for MomentumSample<T> {
562    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563        let mut table = tabled::builder::Builder::new();
564
565        table.push_record(["Sample"]);
566
567        table.push_record(["Loop Momenta", "p_x", "p_y", "p_z"]);
568        // table
569
570        for (index, loop_mom) in self.loop_moms().0.iter().enumerate() {
571            table.push_record([
572                index.to_string(),
573                loop_mom.px.to_string(),
574                loop_mom.py.to_string(),
575                loop_mom.pz.to_string(),
576            ]);
577        }
578
579        table.push_record(["External Momenta", "E", "p_x", "p_y", "p_z"]);
580        for (index, external_mom) in self.external_moms().iter_enumerated() {
581            table.push_record([
582                index.to_string(),
583                external_mom.temporal.to_string(),
584                external_mom.spatial.px.to_string(),
585                external_mom.spatial.py.to_string(),
586                external_mom.spatial.pz.to_string(),
587            ]);
588        }
589
590        table.push_record(["Jacobian".into(), format!("{:+e}", self.sample.jacobian)]);
591        table.build().with(Style::rounded()).fmt(f)
592    }
593}
594
595impl<T: FloatLike> BareMomentumSample<T> {
596    #[inline(never)]
597    pub(crate) fn new(
598        loop_moms: LoopMomenta<F<T>>,
599        loop_mom_cache_id: usize,
600        external_moms: &Externals,
601        external_mom_cache_id: usize,
602        jacobian: F<T>,
603        dependent_momenta_constructor: DependentMomentaConstructor,
604        orientation: Option<usize>,
605    ) -> Result<Self> {
606        let external_moms = external_moms.get_dependent_externals(dependent_momenta_constructor)?;
607
608        Ok(Self {
609            loop_moms,
610            dual_loop_moms: None,
611            loop_mom_cache_id,
612            loop_mom_base_cache_id: loop_mom_cache_id, // Initially same as cache_id
613            external_mom_cache_id,
614            external_mom_base_cache_id: external_mom_cache_id, // Initially same as cache_id
615            external_moms,
616            jacobian,
617            orientation,
618            parameterization_branch: None,
619        })
620    }
621
622    pub(crate) fn one(&self) -> F<T> {
623        if let Some(f) = self.loop_moms.first() {
624            f.px.one()
625        } else if let Some(f) = self.external_moms.first() {
626            f.spatial.px.one()
627        } else {
628            panic!("No momenta in sample")
629        }
630    }
631
632    pub(crate) fn zero(&self) -> F<T> {
633        if let Some(f) = self.loop_moms.first() {
634            f.px.zero()
635        } else if let Some(f) = self.external_moms.first() {
636            f.spatial.px.zero()
637        } else {
638            panic!("No momenta in sample")
639        }
640    }
641
642    /// Cast the sample to a different precision
643    #[inline]
644    fn cast_sample<T2: FloatLike>(&self) -> BareMomentumSample<T2>
645    where
646        F<T2>: From<F<T>>,
647    {
648        BareMomentumSample {
649            loop_mom_cache_id: self.loop_mom_cache_id,
650            loop_mom_base_cache_id: self.loop_mom_base_cache_id,
651            external_mom_cache_id: self.external_mom_cache_id,
652            external_mom_base_cache_id: self.external_mom_base_cache_id,
653            loop_moms: self.loop_moms.iter().map(ThreeMomentum::cast).collect(),
654            dual_loop_moms: self
655                .dual_loop_moms
656                .as_ref()
657                .map(|_dlm| todo!("make sure the cast works if there are hyperdual momenta")),
658            external_moms: self.external_moms.iter().map(FourMomentum::cast).collect(),
659            jacobian: self.jacobian.clone().into(),
660            orientation: self.orientation,
661            parameterization_branch: self.parameterization_branch,
662        }
663    }
664
665    pub(crate) fn higher_precision(&self) -> BareMomentumSample<T::Higher>
666    where
667        T::Higher: FloatLike,
668        T::Lower: FloatLike,
669    {
670        BareMomentumSample {
671            loop_moms: self.loop_moms.iter().map(ThreeMomentum::higher).collect(),
672            dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
673                LoopMomenta::from_iter(dlm.iter().map(|m| m.clone().map(&|x| x.higher())))
674            }),
675            external_moms: self
676                .external_moms
677                .iter()
678                .map(FourMomentum::higher)
679                .collect(),
680            loop_mom_cache_id: self.loop_mom_cache_id,
681            loop_mom_base_cache_id: self.loop_mom_base_cache_id,
682            external_mom_cache_id: self.external_mom_cache_id,
683            external_mom_base_cache_id: self.external_mom_base_cache_id,
684            jacobian: self.jacobian.higher(),
685            orientation: self.orientation,
686            parameterization_branch: self.parameterization_branch,
687        }
688    }
689
690    pub(crate) fn lower_precision(&self) -> BareMomentumSample<T::Lower>
691    where
692        T::Higher: FloatLike,
693        T::Lower: FloatLike,
694    {
695        BareMomentumSample {
696            loop_moms: self.loop_moms.iter().map(ThreeMomentum::lower).collect(),
697            dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
698                LoopMomenta::from_iter(dlm.iter().map(|m| m.clone().map(&|x| x.lower())))
699            }),
700            external_moms: self.external_moms.iter().map(FourMomentum::lower).collect(),
701            jacobian: self.jacobian.lower(),
702            orientation: self.orientation,
703            loop_mom_cache_id: self.loop_mom_cache_id,
704            loop_mom_base_cache_id: self.loop_mom_base_cache_id,
705            external_mom_cache_id: self.external_mom_cache_id,
706            external_mom_base_cache_id: self.external_mom_base_cache_id,
707            parameterization_branch: self.parameterization_branch,
708        }
709    }
710
711    #[inline]
712    pub(crate) fn rotate(
713        &self,
714        rotation: &Rotation,
715        loop_mom_cache_id: usize,
716        external_mom_cache_id: usize,
717    ) -> Self {
718        Self {
719            loop_moms: self.loop_moms.iter().map(|l| l.rotate(rotation)).collect(),
720            dual_loop_moms: self
721                .dual_loop_moms
722                .as_ref()
723                .map(|dlm| LoopMomenta::from_iter(dlm.iter().map(|l| l.rotate(rotation)))),
724            external_moms: self
725                .external_moms
726                .iter()
727                .map(|l| l.rotate(rotation))
728                .collect(),
729            loop_mom_cache_id,
730            loop_mom_base_cache_id: self.loop_mom_base_cache_id, // Preserve base cache ID
731            external_mom_cache_id,
732            external_mom_base_cache_id: self.external_mom_base_cache_id, // Preserve base cache ID
733            jacobian: self.jacobian.clone(),
734            orientation: self.orientation,
735            parameterization_branch: self.parameterization_branch,
736        }
737    }
738
739    #[inline]
740    #[allow(dead_code)]
741    pub(crate) fn rescaled_loop_momenta(&self, factor: &F<T>, subspace: Subspace) -> Self {
742        Self {
743            loop_moms: self.loop_moms.rescale(factor, subspace),
744            dual_loop_moms: self
745                .dual_loop_moms
746                .as_ref()
747                .map(|dlm| dlm.rescale(&new_constant(&dlm[LoopIndex(0)].px, factor), subspace)),
748            loop_mom_cache_id: self.loop_mom_cache_id + 1,
749            loop_mom_base_cache_id: self.loop_mom_base_cache_id, // Preserve base cache ID
750            external_moms: self.external_moms.clone(),
751            external_mom_cache_id: self.external_mom_cache_id,
752            external_mom_base_cache_id: self.external_mom_base_cache_id, // Preserve base cache ID
753            jacobian: self.jacobian.clone(),
754            orientation: self.orientation,
755            parameterization_branch: self.parameterization_branch,
756        }
757    }
758
759    #[inline]
760    pub(crate) fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
761        Self {
762            loop_moms: self.loop_moms.lmb_transform(
763                from,
764                to,
765                &extract_external_spatial(&self.external_moms),
766            ),
767            dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
768                let dual_externals = self
769                    .external_moms
770                    .iter()
771                    .map(|four_mom| {
772                        ThreeMomentum::new(
773                            new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.px),
774                            new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.py),
775                            new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.pz),
776                        )
777                    })
778                    .collect();
779                dlm.lmb_transform(from, to, &dual_externals)
780            }),
781            loop_mom_cache_id: self.loop_mom_cache_id + 1,
782            loop_mom_base_cache_id: self.loop_mom_base_cache_id, // Preserve base cache ID
783            external_moms: self.external_moms.clone(),
784            external_mom_cache_id: self.external_mom_cache_id,
785            external_mom_base_cache_id: self.external_mom_base_cache_id, // Preserve base cache ID
786            jacobian: self.jacobian.clone(),
787            orientation: self.orientation,
788            parameterization_branch: self.parameterization_branch,
789        }
790    }
791}
792
793impl<T: FloatLike> MomentumSample<T> {
794    pub(crate) fn orientations<'a, OID: From<usize> + Copy + IndexLike>(
795        &self,
796        filter: &'a SubSet<OID>,
797        orientations: &'a TiVec<OID, EdgeVec<Orientation>>,
798    ) -> SingleOrAllOrientations<'a, OID>
799    where
800        usize: From<OID>,
801    {
802        if let Some(o) = self.sample.orientation {
803            let id = if filter.is_full() {
804                OID::from(o)
805            } else {
806                filter
807                    .included_iter()
808                    .nth(o)
809                    .unwrap_or_else(|| {
810                        panic!(
811                            "sampled orientation index {o} must resolve within the filtered orientation subset"
812                        )
813                    })
814            };
815            SingleOrAllOrientations::Single {
816                id,
817                orientation: &orientations[id],
818            }
819        } else {
820            SingleOrAllOrientations::All {
821                all: orientations,
822                filter,
823            }
824        }
825    }
826
827    // pub(crate) fn uuid(&self) -> Option<Uuid> {
828    //     if self.rotated_sample.is_some() {
829    //         None
830    //     } else {
831    //         Some(self.uuid)
832    //     }
833    // }
834
835    pub(crate) fn loop_moms(&self) -> &LoopMomenta<F<T>> {
836        &self.sample.loop_moms
837    }
838
839    pub(crate) fn external_moms(&self) -> &ExternalFourMomenta<F<T>> {
840        &self.sample.external_moms
841    }
842
843    pub(crate) fn jacobian(&self) -> F<T> {
844        self.sample.jacobian.clone()
845    }
846
847    pub(crate) fn new(
848        loop_moms: LoopMomenta<F<T>>,
849        loop_mom_cache_id: usize,
850        external_moms: &Externals,
851        external_mom_cache_id: usize,
852        jacobian: F<T>,
853        dependent_momenta_constructor: DependentMomentaConstructor,
854        orientation: Option<usize>,
855    ) -> Result<Self> {
856        // info!("New with ext cache id:{external_mom_cache_id}");
857        Ok(Self {
858            sample: BareMomentumSample::new(
859                loop_moms,
860                loop_mom_cache_id,
861                external_moms,
862                external_mom_cache_id,
863                jacobian,
864                dependent_momenta_constructor,
865                orientation,
866            )?,
867            // uuid: Uuid::new_v4(),
868        })
869    }
870
871    pub(crate) fn one(&self) -> F<T> {
872        self.sample.one()
873    }
874
875    pub(crate) fn zero(&self) -> F<T> {
876        self.sample.zero()
877    }
878
879    /// Cast the sample to a different precision
880    #[inline]
881    pub(crate) fn cast_sample<T2: FloatLike>(&self) -> MomentumSample<T2>
882    where
883        F<T2>: From<F<T>>,
884    {
885        MomentumSample {
886            sample: self.sample.cast_sample(),
887        }
888    }
889
890    pub(crate) fn higher_precision(&self) -> MomentumSample<T::Higher>
891    where
892        T::Higher: FloatLike + Default,
893        T::Lower: FloatLike + Default,
894    {
895        MomentumSample {
896            sample: self.sample.higher_precision(),
897        }
898    }
899
900    pub(crate) fn lower_precision(&self) -> MomentumSample<T::Lower>
901    where
902        T::Lower: FloatLike + Default,
903        T::Higher: FloatLike + Default,
904    {
905        MomentumSample {
906            sample: self.sample.lower_precision(),
907        }
908    }
909
910    #[inline]
911    pub(crate) fn rotate(
912        &self,
913        rotation: &Rotation,
914        loop_mom_cache_id: usize,
915        external_mom_cache_id: usize,
916    ) -> Self {
917        Self {
918            sample: self
919                .sample
920                .rotate(rotation, loop_mom_cache_id, external_mom_cache_id),
921        }
922    }
923
924    #[allow(dead_code)]
925    #[inline]
926    pub(crate) fn rescaled_loop_momenta(&self, factor: &F<T>, subspace: Subspace) -> Self {
927        Self {
928            sample: self.sample.rescaled_loop_momenta(factor, subspace),
929        }
930    }
931
932    #[inline]
933    pub(crate) fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
934        Self {
935            sample: self.sample.lmb_transform(from, to),
936        }
937    }
938}