Skip to main content

gammalooprs/settings/runtime/
kinematic.rs

1pub mod improvement;
2
3use bincode_trait_derive::{Decode, Encode};
4use eyre::eyre;
5use schemars::JsonSchema;
6use serde::{Deserialize, Deserializer, Serialize};
7use spenso::algebra::{algebraic_traits::IsZero, complex::Complex};
8use tabled::{builder::Builder, settings::Style};
9use tracing::debug;
10use typed_index_collections::TiVec;
11
12use crate::{
13    DependentMomentaConstructor, GammaLoopContext,
14    graph::Graph,
15    momentum::sample::{ExternalFourMomenta, ExternalIndex},
16    momentum::signature::ExternalSignature,
17    momentum::{
18        self, Dep, ExternalMomenta, FourMomentum, Helicity, Polarization, Rotatable, SignOrZero,
19    },
20    settings::runtime::kinematic::improvement::{PhaseSpaceImprovementSettings, improve_ps},
21    utils::{
22        ArbPrec, F, FloatLike, RuntimeCache, SamplingPrecision, f128,
23        serde_utils::{IsDefault, is_float},
24    },
25};
26use color_eyre::{Result, Section};
27
28#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
29#[derive(Debug, Clone, Serialize, Encode, Decode, PartialEq, JsonSchema)]
30#[trait_decode(trait= GammaLoopContext)]
31#[serde(default, deny_unknown_fields)]
32pub struct KinematicsSettings {
33    /// Center-of-mass energy used to normalize and construct external momenta.
34    #[serde(skip_serializing_if = "is_float::<64>")]
35    pub e_cm: f64,
36    /// External momentum and helicity configuration evaluated by the integrand.
37    #[serde(skip_serializing_if = "IsDefault::is_default")]
38    pub externals: Externals,
39}
40
41#[derive(Debug, Default, Deserialize)]
42#[serde(default, deny_unknown_fields)]
43struct KinematicsSettingsParser {
44    pub e_cm: Option<f64>,
45    pub externals: Externals,
46}
47
48impl<'de> Deserialize<'de> for KinematicsSettings {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: Deserializer<'de>,
52    {
53        let KinematicsSettingsParser { e_cm, externals } =
54            KinematicsSettingsParser::deserialize(deserializer)?;
55
56        let e_cm = e_cm.unwrap_or_else(|| externals.sane_e_cm_value().0);
57
58        Ok(Self { e_cm, externals })
59    }
60}
61
62impl KinematicsSettings {
63    pub fn random(graph: &Graph, seed: u64) -> Self {
64        Self {
65            e_cm: 64.,
66            externals: graph.random_externals(seed),
67        }
68    }
69}
70
71impl Default for KinematicsSettings {
72    fn default() -> Self {
73        Self {
74            e_cm: 64.,
75            externals: Externals::default(),
76        }
77    }
78}
79
80#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Encode, Decode, JsonSchema)]
81// #[trait_decode(trait= GammaLoopContext)]
82#[serde(tag = "type", content = "data")]
83#[serde(deny_unknown_fields)]
84pub enum Externals {
85    /// Use one fixed list of external momenta and helicities for every evaluation.
86    #[serde(rename = "constant")]
87    Constant {
88        /// External four-momenta; exactly one entry may be marked dependent.
89        momenta: Vec<ExternalMomenta<F<f64>>>,
90        /// Helicity assigned to each external particle in process order.
91        helicities: Vec<Helicity>,
92        /// Optional on-shell phase-space improvement applied before evaluation.
93        #[serde(default, skip_serializing_if = "IsDefault::is_default")]
94        improvement_settings: PhaseSpaceImprovementSettings,
95        // must be set in warmup, but is tied to the constant
96        #[serde(skip)]
97        f_64_cache: Option<TiVec<ExternalIndex, FourMomentum<F<f64>>>>,
98        #[serde(skip)]
99        f_128_cache: Option<TiVec<ExternalIndex, FourMomentum<F<f128>>>>,
100        // Unlike the historical caches above, this new transient field writes
101        // no bytes, preserving the layout of existing saved state archives.
102        #[serde(skip)]
103        arb_cache: RuntimeCache<TiVec<ExternalIndex, FourMomentum<F<ArbPrec>>>>,
104    },
105    // add different type of pdfs here when needed
106}
107
108impl Externals {
109    pub fn sane_e_cm_value(&self) -> F<f64> {
110        match self {
111            Externals::Constant { momenta, .. } => {
112                let mut sum = F(0.0);
113                let mut num_non_zero = 0;
114
115                for m in momenta {
116                    if let ExternalMomenta::Independent(components) = m {
117                        for c in components {
118                            if !c.is_zero() {
119                                sum += c.abs();
120                                num_non_zero += 1;
121                            }
122                        }
123                    }
124                }
125
126                if num_non_zero > 0 {
127                    sum / F(num_non_zero as f64)
128                } else {
129                    F(64.0) // default value for vacuum amplitudes
130                }
131            }
132        }
133    }
134}
135
136impl Rotatable for Externals {
137    fn rotate(&self, rotation: &momentum::Rotation) -> Self {
138        match self {
139            Externals::Constant {
140                momenta,
141                helicities,
142                f_64_cache,
143                f_128_cache,
144                arb_cache,
145                improvement_settings,
146            } => {
147                let momenta = momenta.iter().map(|m| m.rotate(rotation)).collect();
148                let mut rotated_arb = RuntimeCache::default();
149                if let Some(cache) = arb_cache.as_ref() {
150                    rotated_arb.set(cache.iter().map(|m| m.rotate(rotation)).collect());
151                }
152                Externals::Constant {
153                    momenta,
154                    helicities: helicities.clone(),
155                    improvement_settings: improvement_settings.clone(),
156                    arb_cache: rotated_arb,
157                    f_64_cache: f_64_cache
158                        .as_ref()
159                        .map(|cache| cache.iter().map(|m| m.rotate(rotation)).collect()),
160                    f_128_cache: f_128_cache
161                        .as_ref()
162                        .map(|cache| cache.iter().map(|m| m.rotate(rotation)).collect()),
163                }
164            }
165        }
166    }
167}
168
169#[derive(Debug, Clone, PartialEq, Encode, Decode)]
170#[trait_decode(trait= GammaLoopContext)]
171pub enum Polarizations {
172    Constant {
173        polarizations: Vec<Polarization<Complex<F<f64>>>>,
174    },
175    None,
176}
177
178impl Rotatable for Polarizations {
179    fn rotate(&self, rotation: &momentum::Rotation) -> Self {
180        match self {
181            Polarizations::Constant { polarizations } => {
182                let polarizations = polarizations.iter().map(|p| p.rotate(rotation)).collect();
183                Polarizations::Constant { polarizations }
184            }
185            Polarizations::None => Polarizations::None,
186        }
187    }
188}
189
190use thiserror::Error;
191
192#[derive(Error, Debug)]
193pub enum ExternalsValidationError {
194    #[error("There should be exactly one dependent external momentum")]
195    WrongNumberOfDependentMomenta,
196    #[error("Found {0} momenta, expected {1}")]
197    WrongNumberOfMomentaExpected(usize, usize),
198    #[error("Found {0} helicities, expected {1}")]
199    WrongNumberOfHelicities(usize, usize),
200    #[error("Massless vector cannot have zero helicity: pos {0}")]
201    MasslessVectorZeroHelicity(usize),
202    #[error("Spinors cannot have zero helicity at pos {0}")]
203    SpinorZeroHelicity(usize),
204    #[error("Scalars cannot have non-zero helicity at pos {0}")]
205    ScalarNonZeroHelicity(usize),
206    #[error("{0} is an Unsuported external spin for pos {0}")]
207    UnsupportedSpin(isize, usize),
208}
209
210impl Externals {
211    pub fn validate_helicities(
212        &self,
213        spins: &[(isize, bool)],
214    ) -> Result<(), ExternalsValidationError> {
215        match self {
216            Externals::Constant { helicities, .. } => {
217                if helicities.len() == spins.len() {
218                    for (i, (h, (s, is_massless))) in
219                        helicities.iter().zip(spins.iter()).enumerate()
220                    {
221                        let Helicity::Signed(h) = h else {
222                            continue;
223                        };
224                        match *s {
225                            1 => {
226                                if !h.is_zero() {
227                                    return Err(ExternalsValidationError::ScalarNonZeroHelicity(i));
228                                }
229                            }
230                            2 => {
231                                if h.is_zero() {
232                                    return Err(ExternalsValidationError::SpinorZeroHelicity(i));
233                                }
234                            }
235                            3 => {
236                                if h.is_zero() && *is_massless {
237                                    return Err(
238                                        ExternalsValidationError::MasslessVectorZeroHelicity(i),
239                                    );
240                                }
241                            }
242                            s => return Err(ExternalsValidationError::UnsupportedSpin(s, i)),
243                        }
244                    }
245                    Ok(())
246                } else {
247                    Err(ExternalsValidationError::WrongNumberOfHelicities(
248                        helicities.len(),
249                        spins.len(),
250                    ))
251                }
252            }
253        }
254    }
255
256    pub fn set_dependent_at_end(
257        &mut self,
258        signature: &ExternalSignature,
259    ) -> Result<(), ExternalsValidationError> {
260        match self {
261            Externals::Constant { momenta, .. } => {
262                let mut sum: FourMomentum<F<f128>> = FourMomentum::from([F(0.0); 4]).higher();
263                let mut pos_dep = 0;
264                let mut n_dep = 0;
265
266                let mut dependent_sign = SignOrZero::Plus;
267
268                for ((i, m), s) in momenta.iter().enumerate().zip(signature.iter()) {
269                    if let Ok(a) = FourMomentum::try_from(*m) {
270                        sum -= *s * a.higher();
271                    } else {
272                        pos_dep = i;
273                        n_dep += 1;
274                        dependent_sign = *s;
275                    }
276                }
277                if n_dep == 1 {
278                    momenta[pos_dep] = (dependent_sign * sum.lower()).into();
279                    let len = momenta.len();
280                    momenta[len - 1] = ExternalMomenta::Dependent(Dep::Dep);
281                } else if n_dep == 0 {
282                    debug!("No dependent momentum found, adding the sum at the end");
283                    momenta.push(ExternalMomenta::Dependent(Dep::Dep));
284                } else {
285                    return Err(ExternalsValidationError::WrongNumberOfDependentMomenta);
286                }
287
288                let len = momenta.len();
289                if len == signature.len() {
290                    Ok(())
291                } else {
292                    Err(ExternalsValidationError::WrongNumberOfMomentaExpected(
293                        len - 1,
294                        signature.len() - 1,
295                    ))
296                }
297            }
298        }
299    }
300
301    #[inline(never)]
302    // #[comemo::memoize]
303    pub fn get_dependent_externals<T: FloatLike>(
304        &self,
305        dependent_momenta_constructor: DependentMomentaConstructor,
306    ) -> Result<ExternalFourMomenta<F<T>>>
307// where
308    //     T::Higher: PrecisionUpgradable<Lower = T> + FloatLike,
309    {
310        if let Some(cached) = T::try_extract_externals_from_cache(self) {
311            return Ok(cached.clone());
312        }
313        // Sampling has no separate improved-kinematics cache. Round the original
314        // Arb authority once, so its surface geometry uses the same improved
315        // externals as the canonical physical draw, including after rotations.
316        if T::sampling_precision() == SamplingPrecision::Fixed256
317            && let Some(cached) = ArbPrec::try_extract_externals_from_cache(self)
318        {
319            return cached
320                .iter()
321                .map(|p| {
322                    Ok(FourMomentum::from([
323                        F::from_arb(&p.temporal.value.0)?,
324                        F::from_arb(&p.spatial.px.0)?,
325                        F::from_arb(&p.spatial.py.0)?,
326                        F::from_arb(&p.spatial.pz.0)?,
327                    ]))
328                })
329                .collect();
330        }
331
332        match self {
333            Externals::Constant { momenta, .. } => {
334                match dependent_momenta_constructor {
335                    DependentMomentaConstructor::Amplitude(external_signature) => {
336                        if external_signature.is_empty() {
337                            return Ok(vec![].into());
338                        }
339
340                        let mut sum: FourMomentum<F<T>> = FourMomentum::from([
341                            F::<T>::from_f64(0.0),
342                            F::from_f64(0.0),
343                            F::from_f64(0.0),
344                            F::from_f64(0.0),
345                        ]);
346                        // .higher();
347                        let mut pos_dep = external_signature.len() - 1;
348
349                        let mut dependent_sign = SignOrZero::Plus;
350
351                        let mut dependent_momenta = vec![];
352
353                        if momenta.len() != external_signature.len() {
354                            return Err(eyre!(
355                                "External Momentum in input do not match the number of externals"
356                            ))
357                            .with_note(|| {
358                                let mut table = Builder::new();
359                                for m in momenta {
360                                    match m {
361                                        ExternalMomenta::Dependent(_) => {
362                                            table.push_record(["Dependent"])
363                                        }
364                                        ExternalMomenta::Independent([e, x, y, z]) => table
365                                            .push_record([
366                                                e.to_string(),
367                                                x.to_string(),
368                                                y.to_string(),
369                                                z.to_string(),
370                                            ]),
371                                    }
372                                }
373                                format!(
374                                    "External momenta: \n{}\n{}",
375                                    table.build().with(Style::rounded()),
376                                    external_signature
377                                )
378                            });
379                        }
380
381                        for ((i, m), s) in momenta.iter().enumerate().zip(external_signature.iter())
382                        {
383                            if let Ok(a) = FourMomentum::try_from(*m) {
384                                // println!("external{i}: {}", a);
385                                let a = FourMomentum::<F<T>>::from_ff64(&a);
386                                sum -= *s * a.clone(); //.higher();
387                                dependent_momenta.push(a);
388                            } else {
389                                pos_dep = i;
390                                dependent_sign = *s;
391                                dependent_momenta.push(sum.clone()); //.lower());
392                            }
393                        }
394
395                        dependent_momenta[pos_dep] = dependent_sign * sum; //.lower();
396                        let res = dependent_momenta.into();
397
398                        Ok(res)
399                    }
400                    //DependentMomentaConstructor::CrossSection {
401                    //    external_connections,
402                    //} => {
403                    //    assert_eq!(external_connections.len(), momenta.len());
404
405                    //    // set all_to_zero
406                    //    let mut dependent_momenta = (0..momenta.len() * 2)
407                    //        .map(|_| {
408                    //            FourMomentum::<F<T>>::from_ff64(&FourMomentum::from([F(0.0); 4]))
409                    //        })
410                    //        .collect::<ExternalFourMomenta<F<T>>>();
411
412                    //    let incoming_momenta = momenta
413                    //        .iter()
414                    //        .map(|m| {
415                    //            FourMomentum::<F<T>>::from_ff64(
416                    //                &FourMomentum::try_from(*m)
417                    //                    .expect("dependent momenta in cross section not allowed"),
418                    //            )
419                    //        })
420                    //        .collect_vec();
421
422                    //    for (external_connection, incoming_momentum) in
423                    //        external_connections.iter().zip(incoming_momenta)
424                    //    {
425                    //        dependent_momenta[external_connection.incoming_index] =
426                    //            incoming_momentum.clone();
427                    //        dependent_momenta[external_connection.outgoing_index] =
428                    //            incoming_momentum;
429                    //    }
430
431                    //    Ok(dependent_momenta)
432                    //}
433                    DependentMomentaConstructor::CrossSection => {
434                        let dependent_momenta = momenta
435                            .iter()
436                            .map(|m| {
437                                FourMomentum::<F<T>>::from_ff64(
438                                    &FourMomentum::try_from(*m)
439                                        .expect("dependent momenta in None not allowed"),
440                                )
441                            })
442                            .collect::<ExternalFourMomenta<F<T>>>();
443                        Ok(dependent_momenta)
444                    }
445                }
446            }
447        }
448    }
449
450    #[allow(unused_variables)]
451    #[inline]
452    pub fn get_indep_externals(&self) -> Vec<FourMomentum<F<f64>>> {
453        match self {
454            Externals::Constant {
455                momenta,
456                helicities,
457                ..
458            } => {
459                let momenta: Vec<FourMomentum<_>> = momenta
460                    .iter()
461                    .flat_map(|e| FourMomentum::try_from(*e))
462                    .collect();
463                momenta
464            }
465        }
466    }
467
468    pub(crate) fn get_helicities(&self) -> &[Helicity] {
469        match self {
470            Externals::Constant { helicities, .. } => helicities,
471        }
472    }
473
474    pub(crate) fn _pdf(&self, _x_space_point: &[F<f64>]) -> F<f64> {
475        match self {
476            Externals::Constant { .. } => F(1.0),
477        }
478    }
479
480    pub fn improve_and_cache(
481        &mut self,
482        constructor: DependentMomentaConstructor,
483        masses: &TiVec<ExternalIndex, F<f64>>,
484        e_cm: &F<f64>,
485    ) -> Result<()> {
486        // Recompute from the supplied momenta, including after direct Rust
487        // edits. Reading an old improved cache would conceal those changes.
488        let Self::Constant {
489            f_64_cache,
490            f_128_cache,
491            arb_cache,
492            ..
493        } = self;
494        *f_64_cache = None;
495        *f_128_cache = None;
496        arb_cache.invalidate();
497        let dep_momenta_f64 = self.get_dependent_externals::<f64>(constructor)?;
498        let dep_momenta_f128 = self.get_dependent_externals::<f128>(constructor)?;
499        let dep_momenta_arb = self.get_dependent_externals::<ArbPrec>(constructor)?;
500
501        match constructor {
502            DependentMomentaConstructor::Amplitude(signature) => match self {
503                Externals::Constant {
504                    improvement_settings,
505                    f_64_cache,
506                    f_128_cache,
507                    arb_cache,
508                    ..
509                } => {
510                    let improved_f64 = improve_ps(
511                        &dep_momenta_f64,
512                        masses,
513                        signature,
514                        e_cm,
515                        improvement_settings,
516                    )?;
517
518                    let upcasted_masses = masses.iter().map(|m| F::<f128>::from_ff64(*m)).collect();
519
520                    let improved_f128 = improve_ps(
521                        &dep_momenta_f128,
522                        &upcasted_masses,
523                        signature,
524                        &F::<f128>::from_ff64(*e_cm),
525                        improvement_settings,
526                    )?;
527                    let arb_masses = masses
528                        .iter()
529                        .map(|mass| F::<ArbPrec>::from_ff64(*mass))
530                        .collect();
531                    let improved_arb = improve_ps(
532                        &dep_momenta_arb,
533                        &arb_masses,
534                        signature,
535                        &F::<ArbPrec>::from_ff64(*e_cm),
536                        improvement_settings,
537                    )?;
538                    *f_64_cache = Some(improved_f64);
539                    *f_128_cache = Some(improved_f128);
540                    arb_cache.set(improved_arb);
541                    Ok(())
542                }
543            },
544            // todo, improve the on-shellness
545            DependentMomentaConstructor::CrossSection { .. } => Ok(()),
546        }
547    }
548}
549
550#[test]
551fn external_inv() {
552    let mut ext = Externals::Constant {
553        momenta: vec![[F(1.), F(2.), F(3.), F(4.)].into(); 3],
554        helicities: vec![Helicity::PLUS; 4],
555        f_64_cache: None,
556        f_128_cache: None,
557        arb_cache: Default::default(),
558        improvement_settings: PhaseSpaceImprovementSettings::default(),
559    };
560
561    let signs: ExternalSignature = [1i8, 1, 1, 1].into_iter().collect();
562    ext.set_dependent_at_end(&signs).unwrap();
563
564    let momenta = vec![
565        ExternalMomenta::Dependent(Dep::Dep),
566        [F(1.), F(2.), F(3.), F(4.)].into(),
567        [F(1.), F(2.), F(3.), F(4.)].into(),
568        [F(-3.), F(-6.), F(-9.), F(-12.)].into(),
569    ];
570    let mut ext2 = Externals::Constant {
571        momenta,
572        helicities: vec![Helicity::PLUS; 4],
573        f_64_cache: None,
574        f_128_cache: None,
575        arb_cache: Default::default(),
576        improvement_settings: PhaseSpaceImprovementSettings::default(),
577    };
578
579    ext2.set_dependent_at_end(&signs).unwrap();
580
581    assert_eq!(ext, ext2);
582}
583
584impl Default for Externals {
585    fn default() -> Self {
586        Externals::Constant {
587            momenta: vec![],
588            helicities: vec![],
589            f_64_cache: None,
590            f_128_cache: None,
591            arb_cache: Default::default(),
592            improvement_settings: PhaseSpaceImprovementSettings::default(),
593        }
594    }
595}