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