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///
371#[pyclass(name = "PolAngle", module = "laddu")]
372#[derive(Clone, Serialize, Deserialize)]
373pub struct PyPolAngle(pub PolAngle);
374
375#[pymethods]
376impl PyPolAngle {
377 #[new]
378 fn new(beam: usize, recoil: Vec<usize>) -> Self {
379 Self(PolAngle::new(beam, &recoil))
380 }
381 /// The value of this Variable for the given Event
382 ///
383 /// Parameters
384 /// ----------
385 /// event : Event
386 /// The Event upon which the Variable is calculated
387 ///
388 /// Returns
389 /// -------
390 /// value : float
391 /// The value of the Variable for the given `event`
392 ///
393 fn value(&self, event: &PyEvent) -> Float {
394 self.0.value(&event.0)
395 }
396 /// All values of this Variable on the given Dataset
397 ///
398 /// Parameters
399 /// ----------
400 /// dataset : Dataset
401 /// The Dataset upon which the Variable is calculated
402 ///
403 /// Returns
404 /// -------
405 /// values : array_like
406 /// The values of the Variable for each Event in the given `dataset`
407 ///
408 fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
409 PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
410 }
411}
412
413/// The magnitude of the given particle's polarization vector
414///
415/// This Variable simply represents the magnitude of the polarization vector of the particle
416/// with the index `beam`
417///
418/// Parameters
419/// ----------
420/// beam : int
421/// The index of the `beam` particle
422///
423/// See Also
424/// --------
425/// laddu.utils.vectors.Vector3.mag
426///
427#[pyclass(name = "PolMagnitude", module = "laddu")]
428#[derive(Clone, Serialize, Deserialize)]
429pub struct PyPolMagnitude(pub PolMagnitude);
430
431#[pymethods]
432impl PyPolMagnitude {
433 #[new]
434 fn new(beam: usize) -> Self {
435 Self(PolMagnitude::new(beam))
436 }
437 /// The value of this Variable for the given Event
438 ///
439 /// Parameters
440 /// ----------
441 /// event : Event
442 /// The Event upon which the Variable is calculated
443 ///
444 /// Returns
445 /// -------
446 /// value : float
447 /// The value of the Variable for the given `event`
448 ///
449 fn value(&self, event: &PyEvent) -> Float {
450 self.0.value(&event.0)
451 }
452 /// All values of this Variable on the given Dataset
453 ///
454 /// Parameters
455 /// ----------
456 /// dataset : Dataset
457 /// The Dataset upon which the Variable is calculated
458 ///
459 /// Returns
460 /// -------
461 /// values : array_like
462 /// The values of the Variable for each Event in the given `dataset`
463 ///
464 fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
465 PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
466 }
467}
468
469/// A Variable used to define both the polarization angle and magnitude of the given particle``
470///
471/// This class combines ``laddu.PolAngle`` and ``laddu.PolMagnitude`` into a single
472/// object
473///
474/// Parameters
475/// ----------
476/// beam : int
477/// The index of the `beam` particle
478/// recoil : list of int
479/// Indices of particles which are combined to form the recoiling particle (particles which
480/// are not `beam` or part of the `resonance`)
481///
482/// See Also
483/// --------
484/// laddu.PolAngle
485/// laddu.PolMagnitude
486///
487#[pyclass(name = "Polarization", module = "laddu")]
488#[derive(Clone)]
489pub struct PyPolarization(pub Polarization);
490#[pymethods]
491impl PyPolarization {
492 #[new]
493 fn new(beam: usize, recoil: Vec<usize>) -> Self {
494 PyPolarization(Polarization::new(beam, &recoil))
495 }
496 /// The Variable representing the magnitude of the polarization vector
497 ///
498 /// Returns
499 /// -------
500 /// PolMagnitude
501 ///
502 #[getter]
503 fn pol_magnitude(&self) -> PyPolMagnitude {
504 PyPolMagnitude(self.0.pol_magnitude)
505 }
506 /// The Variable representing the polar angle of the polarization vector
507 ///
508 /// Returns
509 /// -------
510 /// PolAngle
511 ///
512 #[getter]
513 fn pol_angle(&self) -> PyPolAngle {
514 PyPolAngle(self.0.pol_angle.clone())
515 }
516}
517
518/// Mandelstam variables s, t, and u
519///
520/// By convention, the metric is chosen to be :math:`(+---)` and the variables are defined as follows
521/// (ignoring factors of :math:`c`):
522///
523/// .. math:: s = (p_1 + p_2)^2 = (p_3 + p_4)^2
524///
525/// .. math:: t = (p_1 - p_3)^2 = (p_4 - p_2)^2
526///
527/// .. math:: u = (p_1 - p_4)^2 = (p_3 - p_2)^2
528///
529/// Parameters
530/// ----------
531/// p1: list of int
532/// The indices of particles to combine to create :math:`p_1` in the diagram
533/// p2: list of int
534/// The indices of particles to combine to create :math:`p_2` in the diagram
535/// p3: list of int
536/// The indices of particles to combine to create :math:`p_3` in the diagram
537/// p4: list of int
538/// The indices of particles to combine to create :math:`p_4` in the diagram
539/// channel: {'s', 't', 'u', 'S', 'T', 'U'}
540/// The Mandelstam channel to calculate
541///
542/// Raises
543/// ------
544/// Exception
545/// If more than one particle list is empty
546/// ValueError
547/// If `channel` is not one of the valid options
548///
549/// Notes
550/// -----
551/// At most one of the input particles may be omitted by using an empty list. This will cause
552/// the calculation to use whichever equality listed above does not contain that particle.
553///
554/// By default, the first equality is used if no particle lists are empty.
555///
556#[pyclass(name = "Mandelstam", module = "laddu")]
557#[derive(Clone, Serialize, Deserialize)]
558pub struct PyMandelstam(pub Mandelstam);
559
560#[pymethods]
561impl PyMandelstam {
562 #[new]
563 fn new(
564 p1: Vec<usize>,
565 p2: Vec<usize>,
566 p3: Vec<usize>,
567 p4: Vec<usize>,
568 channel: &str,
569 ) -> PyResult<Self> {
570 Ok(Self(Mandelstam::new(p1, p2, p3, p4, channel.parse()?)?))
571 }
572 /// The value of this Variable for the given Event
573 ///
574 /// Parameters
575 /// ----------
576 /// event : Event
577 /// The Event upon which the Variable is calculated
578 ///
579 /// Returns
580 /// -------
581 /// value : float
582 /// The value of the Variable for the given `event`
583 ///
584 fn value(&self, event: &PyEvent) -> Float {
585 self.0.value(&event.0)
586 }
587 /// All values of this Variable on the given Dataset
588 ///
589 /// Parameters
590 /// ----------
591 /// dataset : Dataset
592 /// The Dataset upon which the Variable is calculated
593 ///
594 /// Returns
595 /// -------
596 /// values : array_like
597 /// The values of the Variable for each Event in the given `dataset`
598 ///
599 fn value_on<'py>(&self, py: Python<'py>, dataset: &PyDataset) -> Bound<'py, PyArray1<Float>> {
600 PyArray1::from_slice(py, &self.0.value_on(&dataset.0))
601 }
602}
603
604#[typetag::serde]
605impl Variable for PyVariable {
606 fn value_on(&self, dataset: &Arc<Dataset>) -> Vec<Float> {
607 match self {
608 PyVariable::Mass(mass) => mass.0.value_on(dataset),
609 PyVariable::CosTheta(cos_theta) => cos_theta.0.value_on(dataset),
610 PyVariable::Phi(phi) => phi.0.value_on(dataset),
611 PyVariable::PolAngle(pol_angle) => pol_angle.0.value_on(dataset),
612 PyVariable::PolMagnitude(pol_magnitude) => pol_magnitude.0.value_on(dataset),
613 PyVariable::Mandelstam(mandelstam) => mandelstam.0.value_on(dataset),
614 }
615 }
616
617 fn value(&self, event: &Event) -> Float {
618 match self {
619 PyVariable::Mass(mass) => mass.0.value(event),
620 PyVariable::CosTheta(cos_theta) => cos_theta.0.value(event),
621 PyVariable::Phi(phi) => phi.0.value(event),
622 PyVariable::PolAngle(pol_angle) => pol_angle.0.value(event),
623 PyVariable::PolMagnitude(pol_magnitude) => pol_magnitude.0.value(event),
624 PyVariable::Mandelstam(mandelstam) => mandelstam.0.value(event),
625 }
626 }
627}