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}