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