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}