laddu_python/utils/
variables.rs

1use crate::data::{PyDataset, PyEvent};
2use laddu_core::{
3    data::{Dataset, Event},
4    traits::Variable,
5    utils::variables::{
6        Angles, CosTheta, Mandelstam, Mass, Phi, PolAngle, PolMagnitude, Polarization,
7    },
8    Float,
9};
10use numpy::PyArray1;
11use pyo3::prelude::*;
12use serde::{Deserialize, Serialize};
13use std::fmt::{Debug, Display};
14
15#[derive(FromPyObject, Clone, Serialize, Deserialize)]
16pub enum PyVariable {
17    #[pyo3(transparent)]
18    Mass(PyMass),
19    #[pyo3(transparent)]
20    CosTheta(PyCosTheta),
21    #[pyo3(transparent)]
22    Phi(PyPhi),
23    #[pyo3(transparent)]
24    PolAngle(PyPolAngle),
25    #[pyo3(transparent)]
26    PolMagnitude(PyPolMagnitude),
27    #[pyo3(transparent)]
28    Mandelstam(PyMandelstam),
29}
30
31impl Debug for PyVariable {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Mass(v) => write!(f, "{:?}", v.0),
35            Self::CosTheta(v) => write!(f, "{:?}", v.0),
36            Self::Phi(v) => write!(f, "{:?}", v.0),
37            Self::PolAngle(v) => write!(f, "{:?}", v.0),
38            Self::PolMagnitude(v) => write!(f, "{:?}", v.0),
39            Self::Mandelstam(v) => write!(f, "{:?}", v.0),
40        }
41    }
42}
43impl Display for PyVariable {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::Mass(v) => write!(f, "{}", v.0),
47            Self::CosTheta(v) => write!(f, "{}", v.0),
48            Self::Phi(v) => write!(f, "{}", v.0),
49            Self::PolAngle(v) => write!(f, "{}", v.0),
50            Self::PolMagnitude(v) => write!(f, "{}", v.0),
51            Self::Mandelstam(v) => write!(f, "{}", v.0),
52        }
53    }
54}
55
56/// The invariant mass of an arbitrary combination of constituent particles in an Event
57///
58/// This variable is calculated by summing up the 4-momenta of each particle listed by index in
59/// `constituents` and taking the invariant magnitude of the resulting 4-vector.
60///
61/// Parameters
62/// ----------
63/// constituents : list of int
64///     The indices of particles to combine to create the final 4-momentum
65///
66/// See Also
67/// --------
68/// laddu.utils.vectors.Vec4.m
69///
70#[pyclass(name = "Mass", module = "laddu")]
71#[derive(Clone, Serialize, Deserialize)]
72pub struct PyMass(pub Mass);
73
74#[pymethods]
75impl PyMass {
76    #[new]
77    fn new(constituents: Vec<usize>) -> Self {
78        Self(Mass::new(&constituents))
79    }
80    /// The value of this Variable for the given Event
81    ///
82    /// Parameters
83    /// ----------
84    /// event : Event
85    ///     The Event upon which the Variable is calculated
86    ///
87    /// Returns
88    /// -------
89    /// value : float
90    ///     The value of the Variable for the given `event`
91    ///
92    fn value(&self, event: &PyEvent) -> Float {
93        self.0.value(&event.0)
94    }
95    /// All values of this Variable on the given Dataset
96    ///
97    /// Parameters
98    /// ----------
99    /// dataset : Dataset
100    ///     The Dataset upon which the Variable is calculated
101    ///
102    /// Returns
103    /// -------
104    /// values : array_like
105    ///     The values of the Variable for each Event in the given `dataset`
106    ///
107    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
108        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
109    }
110    fn __repr__(&self) -> String {
111        format!("{:?}", self.0)
112    }
113    fn __str__(&self) -> String {
114        format!("{}", self.0)
115    }
116}
117
118/// The cosine of the polar decay angle in the rest frame of the given `resonance`
119///
120/// This Variable is calculated by forming the given frame (helicity or Gottfried-Jackson) and
121/// calculating the spherical angles according to one of the decaying `daughter` particles.
122///
123/// The helicity frame is defined in terms of the following Cartesian axes in the rest frame of
124/// the `resonance`:
125///
126/// .. math:: \hat{z} \propto -\vec{p}'_{\text{recoil}}
127/// .. math:: \hat{y} \propto \vec{p}_{\text{beam}} \times (-\vec{p}_{\text{recoil}})
128/// .. math:: \hat{x} = \hat{y} \times \hat{z}
129///
130/// where primed vectors are in the rest frame of the `resonance` and unprimed vectors are in
131/// the center-of-momentum frame.
132///
133/// The Gottfried-Jackson frame differs only in the definition of :math:`\hat{z}`:
134///
135/// .. math:: \hat{z} \propto \vec{p}'_{\text{beam}}
136///
137/// Parameters
138/// ----------
139/// beam : int
140///     The index of the `beam` particle
141/// recoil : list of int
142///     Indices of particles which are combined to form the recoiling particle (particles which
143///     are not `beam` or part of the `resonance`)
144/// daughter : list of int
145///     Indices of particles which are combined to form one of the decay products of the
146///     `resonance`
147/// resonance : list of int
148///     Indices of particles which are combined to form the `resonance`
149/// frame : {'Helicity', 'HX', 'HEL', 'GottfriedJackson', 'Gottfried Jackson', 'GJ', 'Gottfried-Jackson'}
150///     The frame to use in the  calculation
151///
152/// Raises
153/// ------
154/// ValueError
155///     If `frame` is not one of the valid options
156///
157/// See Also
158/// --------
159/// laddu.utils.vectors.Vec3.costheta
160///
161#[pyclass(name = "CosTheta", module = "laddu")]
162#[derive(Clone, Serialize, Deserialize)]
163pub struct PyCosTheta(pub CosTheta);
164
165#[pymethods]
166impl PyCosTheta {
167    #[new]
168    #[pyo3(signature=(beam, recoil, daughter, resonance, frame="Helicity"))]
169    fn new(
170        beam: usize,
171        recoil: Vec<usize>,
172        daughter: Vec<usize>,
173        resonance: Vec<usize>,
174        frame: &str,
175    ) -> PyResult<Self> {
176        Ok(Self(CosTheta::new(
177            beam,
178            &recoil,
179            &daughter,
180            &resonance,
181            frame.parse()?,
182        )))
183    }
184    /// The value of this Variable for the given Event
185    ///
186    /// Parameters
187    /// ----------
188    /// event : Event
189    ///     The Event upon which the Variable is calculated
190    ///
191    /// Returns
192    /// -------
193    /// value : float
194    ///     The value of the Variable for the given `event`
195    ///
196    fn value(&self, event: &PyEvent) -> Float {
197        self.0.value(&event.0)
198    }
199    /// All values of this Variable on the given Dataset
200    ///
201    /// Parameters
202    /// ----------
203    /// dataset : Dataset
204    ///     The Dataset upon which the Variable is calculated
205    ///
206    /// Returns
207    /// -------
208    /// values : array_like
209    ///     The values of the Variable for each Event in the given `dataset`
210    ///
211    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
212        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
213    }
214    fn __repr__(&self) -> String {
215        format!("{:?}", self.0)
216    }
217    fn __str__(&self) -> String {
218        format!("{}", self.0)
219    }
220}
221
222/// The aziumuthal decay angle in the rest frame of the given `resonance`
223///
224/// This Variable is calculated by forming the given frame (helicity or Gottfried-Jackson) and
225/// calculating the spherical angles according to one of the decaying `daughter` particles.
226///
227/// The helicity frame is defined in terms of the following Cartesian axes in the rest frame of
228/// the `resonance`:
229///
230/// .. math:: \hat{z} \propto -\vec{p}'_{\text{recoil}}
231/// .. math:: \hat{y} \propto \vec{p}_{\text{beam}} \times (-\vec{p}_{\text{recoil}})
232/// .. math:: \hat{x} = \hat{y} \times \hat{z}
233///
234/// where primed vectors are in the rest frame of the `resonance` and unprimed vectors are in
235/// the center-of-momentum frame.
236///
237/// The Gottfried-Jackson frame differs only in the definition of :math:`\hat{z}`:
238///
239/// .. math:: \hat{z} \propto \vec{p}'_{\text{beam}}
240///
241/// Parameters
242/// ----------
243/// beam : int
244///     The index of the `beam` particle
245/// recoil : list of int
246///     Indices of particles which are combined to form the recoiling particle (particles which
247///     are not `beam` or part of the `resonance`)
248/// daughter : list of int
249///     Indices of particles which are combined to form one of the decay products of the
250///     `resonance`
251/// resonance : list of int
252///     Indices of particles which are combined to form the `resonance`
253/// frame : {'Helicity', 'HX', 'HEL', 'GottfriedJackson', 'Gottfried Jackson', 'GJ', 'Gottfried-Jackson'}
254///     The frame to use in the  calculation
255///
256/// Raises
257/// ------
258/// ValueError
259///     If `frame` is not one of the valid options
260///
261///
262/// See Also
263/// --------
264/// laddu.utils.vectors.Vec3.phi
265///
266#[pyclass(name = "Phi", module = "laddu")]
267#[derive(Clone, Serialize, Deserialize)]
268pub struct PyPhi(pub Phi);
269
270#[pymethods]
271impl PyPhi {
272    #[new]
273    #[pyo3(signature=(beam, recoil, daughter, resonance, frame="Helicity"))]
274    fn new(
275        beam: usize,
276        recoil: Vec<usize>,
277        daughter: Vec<usize>,
278        resonance: Vec<usize>,
279        frame: &str,
280    ) -> PyResult<Self> {
281        Ok(Self(Phi::new(
282            beam,
283            &recoil,
284            &daughter,
285            &resonance,
286            frame.parse()?,
287        )))
288    }
289    /// The value of this Variable for the given Event
290    ///
291    /// Parameters
292    /// ----------
293    /// event : Event
294    ///     The Event upon which the Variable is calculated
295    ///
296    /// Returns
297    /// -------
298    /// value : float
299    ///     The value of the Variable for the given `event`
300    ///
301    fn value(&self, event: &PyEvent) -> Float {
302        self.0.value(&event.0)
303    }
304    /// All values of this Variable on the given Dataset
305    ///
306    /// Parameters
307    /// ----------
308    /// dataset : Dataset
309    ///     The Dataset upon which the Variable is calculated
310    ///
311    /// Returns
312    /// -------
313    /// values : array_like
314    ///     The values of the Variable for each Event in the given `dataset`
315    ///
316    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
317        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
318    }
319    fn __repr__(&self) -> String {
320        format!("{:?}", self.0)
321    }
322    fn __str__(&self) -> String {
323        format!("{}", self.0)
324    }
325}
326
327/// A Variable used to define both spherical decay angles in the given frame
328///
329/// This class combines ``laddu.CosTheta`` and ``laddu.Phi`` into a single
330/// object
331///
332/// Parameters
333/// ----------
334/// beam : int
335///     The index of the `beam` particle
336/// recoil : list of int
337///     Indices of particles which are combined to form the recoiling particle (particles which
338///     are not `beam` or part of the `resonance`)
339/// daughter : list of int
340///     Indices of particles which are combined to form one of the decay products of the
341///     `resonance`
342/// resonance : list of int
343///     Indices of particles which are combined to form the `resonance`
344/// frame : {'Helicity', 'HX', 'HEL', 'GottfriedJackson', 'Gottfried Jackson', 'GJ', 'Gottfried-Jackson'}
345///     The frame to use in the  calculation
346///
347/// Raises
348/// ------
349/// ValueError
350///     If `frame` is not one of the valid options
351///
352/// See Also
353/// --------
354/// laddu.CosTheta
355/// laddu.Phi
356///
357#[pyclass(name = "Angles", module = "laddu")]
358#[derive(Clone)]
359pub struct PyAngles(pub Angles);
360#[pymethods]
361impl PyAngles {
362    #[new]
363    #[pyo3(signature=(beam, recoil, daughter, resonance, frame="Helicity"))]
364    fn new(
365        beam: usize,
366        recoil: Vec<usize>,
367        daughter: Vec<usize>,
368        resonance: Vec<usize>,
369        frame: &str,
370    ) -> PyResult<Self> {
371        Ok(Self(Angles::new(
372            beam,
373            &recoil,
374            &daughter,
375            &resonance,
376            frame.parse()?,
377        )))
378    }
379    /// The Variable representing the cosine of the polar spherical decay angle
380    ///
381    /// Returns
382    /// -------
383    /// CosTheta
384    ///
385    #[getter]
386    fn costheta(&self) -> PyCosTheta {
387        PyCosTheta(self.0.costheta.clone())
388    }
389    // The Variable representing the polar azimuthal decay angle
390    //
391    // Returns
392    // -------
393    // Phi
394    //
395    #[getter]
396    fn phi(&self) -> PyPhi {
397        PyPhi(self.0.phi.clone())
398    }
399    fn __repr__(&self) -> String {
400        format!("{:?}", self.0)
401    }
402    fn __str__(&self) -> String {
403        format!("{}", self.0)
404    }
405}
406
407/// The polar angle of the given polarization vector with respect to the production plane
408///
409/// The `beam` and `recoil` particles define the plane of production, and this Variable
410/// describes the polar angle of the `beam` relative to this plane
411///
412/// Parameters
413/// ----------
414/// beam : int
415///     The index of the `beam` particle
416/// recoil : list of int
417///     Indices of particles which are combined to form the recoiling particle (particles which
418///     are not `beam` or part of the `resonance`)
419/// beam_polarization : int
420///     The index of the auxiliary vector in storing the `beam` particle's polarization
421///
422#[pyclass(name = "PolAngle", module = "laddu")]
423#[derive(Clone, Serialize, Deserialize)]
424pub struct PyPolAngle(pub PolAngle);
425
426#[pymethods]
427impl PyPolAngle {
428    #[new]
429    fn new(beam: usize, recoil: Vec<usize>, beam_polarization: usize) -> Self {
430        Self(PolAngle::new(beam, &recoil, beam_polarization))
431    }
432    /// The value of this Variable for the given Event
433    ///
434    /// Parameters
435    /// ----------
436    /// event : Event
437    ///     The Event upon which the Variable is calculated
438    ///
439    /// Returns
440    /// -------
441    /// value : float
442    ///     The value of the Variable for the given `event`
443    ///
444    fn value(&self, event: &PyEvent) -> Float {
445        self.0.value(&event.0)
446    }
447    /// All values of this Variable on the given Dataset
448    ///
449    /// Parameters
450    /// ----------
451    /// dataset : Dataset
452    ///     The Dataset upon which the Variable is calculated
453    ///
454    /// Returns
455    /// -------
456    /// values : array_like
457    ///     The values of the Variable for each Event in the given `dataset`
458    ///
459    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
460        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
461    }
462    fn __repr__(&self) -> String {
463        format!("{:?}", self.0)
464    }
465    fn __str__(&self) -> String {
466        format!("{}", self.0)
467    }
468}
469
470/// The magnitude of the given particle's polarization vector
471///
472/// This Variable simply represents the magnitude of the polarization vector of the particle
473/// with the index `beam`
474///
475/// Parameters
476/// ----------
477/// beam_polarization : int
478///     The index of the auxiliary vector in storing the `beam` particle's polarization
479///
480/// See Also
481/// --------
482/// laddu.utils.vectors.Vec3.mag
483///
484#[pyclass(name = "PolMagnitude", module = "laddu")]
485#[derive(Clone, Serialize, Deserialize)]
486pub struct PyPolMagnitude(pub PolMagnitude);
487
488#[pymethods]
489impl PyPolMagnitude {
490    #[new]
491    fn new(beam_polarization: usize) -> Self {
492        Self(PolMagnitude::new(beam_polarization))
493    }
494    /// The value of this Variable for the given Event
495    ///
496    /// Parameters
497    /// ----------
498    /// event : Event
499    ///     The Event upon which the Variable is calculated
500    ///
501    /// Returns
502    /// -------
503    /// value : float
504    ///     The value of the Variable for the given `event`
505    ///
506    fn value(&self, event: &PyEvent) -> Float {
507        self.0.value(&event.0)
508    }
509    /// All values of this Variable on the given Dataset
510    ///
511    /// Parameters
512    /// ----------
513    /// dataset : Dataset
514    ///     The Dataset upon which the Variable is calculated
515    ///
516    /// Returns
517    /// -------
518    /// values : array_like
519    ///     The values of the Variable for each Event in the given `dataset`
520    ///
521    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
522        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
523    }
524    fn __repr__(&self) -> String {
525        format!("{:?}", self.0)
526    }
527    fn __str__(&self) -> String {
528        format!("{}", self.0)
529    }
530}
531
532/// A Variable used to define both the polarization angle and magnitude of the given particle``
533///
534/// This class combines ``laddu.PolAngle`` and ``laddu.PolMagnitude`` into a single
535/// object
536///
537/// Parameters
538/// ----------
539/// beam : int
540///     The index of the `beam` particle
541/// recoil : list of int
542///     Indices of particles which are combined to form the recoiling particle (particles which
543///     are not `beam` or part of the `resonance`)
544/// beam_polarization : int
545///     The index of the auxiliary vector in storing the `beam` particle's polarization
546///
547/// See Also
548/// --------
549/// laddu.PolAngle
550/// laddu.PolMagnitude
551///
552#[pyclass(name = "Polarization", module = "laddu")]
553#[derive(Clone)]
554pub struct PyPolarization(pub Polarization);
555#[pymethods]
556impl PyPolarization {
557    #[new]
558    fn new(beam: usize, recoil: Vec<usize>, beam_polarization: usize) -> Self {
559        PyPolarization(Polarization::new(beam, &recoil, beam_polarization))
560    }
561    /// The Variable representing the magnitude of the polarization vector
562    ///
563    /// Returns
564    /// -------
565    /// PolMagnitude
566    ///
567    #[getter]
568    fn pol_magnitude(&self) -> PyPolMagnitude {
569        PyPolMagnitude(self.0.pol_magnitude)
570    }
571    /// The Variable representing the polar angle of the polarization vector
572    ///
573    /// Returns
574    /// -------
575    /// PolAngle
576    ///
577    #[getter]
578    fn pol_angle(&self) -> PyPolAngle {
579        PyPolAngle(self.0.pol_angle.clone())
580    }
581    fn __repr__(&self) -> String {
582        format!("{:?}", self.0)
583    }
584    fn __str__(&self) -> String {
585        format!("{}", self.0)
586    }
587}
588
589/// Mandelstam variables s, t, and u
590///
591/// By convention, the metric is chosen to be :math:`(+---)` and the variables are defined as follows
592/// (ignoring factors of :math:`c`):
593///
594/// .. math:: s = (p_1 + p_2)^2 = (p_3 + p_4)^2
595///
596/// .. math:: t = (p_1 - p_3)^2 = (p_4 - p_2)^2
597///
598/// .. math:: u = (p_1 - p_4)^2 = (p_3 - p_2)^2
599///
600/// Parameters
601/// ----------
602/// p1: list of int
603///     The indices of particles to combine to create :math:`p_1` in the diagram
604/// p2: list of int
605///     The indices of particles to combine to create :math:`p_2` in the diagram
606/// p3: list of int
607///     The indices of particles to combine to create :math:`p_3` in the diagram
608/// p4: list of int
609///     The indices of particles to combine to create :math:`p_4` in the diagram
610/// channel: {'s', 't', 'u', 'S', 'T', 'U'}
611///     The Mandelstam channel to calculate
612///
613/// Raises
614/// ------
615/// Exception
616///     If more than one particle list is empty
617/// ValueError
618///     If `channel` is not one of the valid options
619///
620/// Notes
621/// -----
622/// At most one of the input particles may be omitted by using an empty list. This will cause
623/// the calculation to use whichever equality listed above does not contain that particle.
624///
625/// By default, the first equality is used if no particle lists are empty.
626///
627#[pyclass(name = "Mandelstam", module = "laddu")]
628#[derive(Clone, Serialize, Deserialize)]
629pub struct PyMandelstam(pub Mandelstam);
630
631#[pymethods]
632impl PyMandelstam {
633    #[new]
634    fn new(
635        p1: Vec<usize>,
636        p2: Vec<usize>,
637        p3: Vec<usize>,
638        p4: Vec<usize>,
639        channel: &str,
640    ) -> PyResult<Self> {
641        Ok(Self(Mandelstam::new(p1, p2, p3, p4, channel.parse()?)?))
642    }
643    /// The value of this Variable for the given Event
644    ///
645    /// Parameters
646    /// ----------
647    /// event : Event
648    ///     The Event upon which the Variable is calculated
649    ///
650    /// Returns
651    /// -------
652    /// value : float
653    ///     The value of the Variable for the given `event`
654    ///
655    fn value(&self, event: &PyEvent) -> Float {
656        self.0.value(&event.0)
657    }
658    /// All values of this Variable on the given Dataset
659    ///
660    /// Parameters
661    /// ----------
662    /// dataset : Dataset
663    ///     The Dataset upon which the Variable is calculated
664    ///
665    /// Returns
666    /// -------
667    /// values : array_like
668    ///     The values of the Variable for each Event in the given `dataset`
669    ///
670    fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
671        PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
672    }
673    fn __repr__(&self) -> String {
674        format!("{:?}", self.0)
675    }
676    fn __str__(&self) -> String {
677        format!("{}", self.0)
678    }
679}
680
681#[typetag::serde]
682impl Variable for PyVariable {
683    fn value_on(&self, dataset: &Dataset) -> Vec<Float> {
684        match self {
685            PyVariable::Mass(mass) => mass.0.value_on(dataset),
686            PyVariable::CosTheta(cos_theta) => cos_theta.0.value_on(dataset),
687            PyVariable::Phi(phi) => phi.0.value_on(dataset),
688            PyVariable::PolAngle(pol_angle) => pol_angle.0.value_on(dataset),
689            PyVariable::PolMagnitude(pol_magnitude) => pol_magnitude.0.value_on(dataset),
690            PyVariable::Mandelstam(mandelstam) => mandelstam.0.value_on(dataset),
691        }
692    }
693
694    fn value(&self, event: &Event) -> Float {
695        match self {
696            PyVariable::Mass(mass) => mass.0.value(event),
697            PyVariable::CosTheta(cos_theta) => cos_theta.0.value(event),
698            PyVariable::Phi(phi) => phi.0.value(event),
699            PyVariable::PolAngle(pol_angle) => pol_angle.0.value(event),
700            PyVariable::PolMagnitude(pol_magnitude) => pol_magnitude.0.value(event),
701            PyVariable::Mandelstam(mandelstam) => mandelstam.0.value(event),
702        }
703    }
704}