Skip to main content

laddu_physics/
vectors.rs

1use std::{
2    fmt::Display,
3    ops::{Add, Div, Mul, Sub},
4};
5
6use approx::{AbsDiffEq, RelativeEq};
7use auto_ops::{impl_op_ex, impl_op_ex_commutative};
8use laddu_expr::{Expr, P4Component, atan2, event_p4_component, vector};
9use nalgebra::{Vector3, Vector4};
10use serde::{Deserialize, Serialize};
11
12use crate::{LadduPhysicsError, LadduPhysicsResult};
13
14fn dot3<'a, T>(lhs: [&'a T; 3], rhs: [&'a T; 3]) -> T
15where
16    &'a T: Mul<&'a T, Output = T>,
17    T: Add<T, Output = T>,
18{
19    lhs[0] * rhs[0] + lhs[1] * rhs[1] + lhs[2] * rhs[2]
20}
21
22fn cross3<'a, T>(lhs: [&'a T; 3], rhs: [&'a T; 3]) -> [T; 3]
23where
24    &'a T: Mul<&'a T, Output = T>,
25    T: Sub<T, Output = T>,
26{
27    [
28        lhs[1] * rhs[2] - rhs[1] * lhs[2],
29        lhs[2] * rhs[0] - rhs[2] * lhs[0],
30        lhs[0] * rhs[1] - rhs[0] * lhs[1],
31    ]
32}
33
34fn lorentz_dot4<'a, T>(lhs: [&'a T; 4], rhs: [&'a T; 4]) -> T
35where
36    &'a T: Mul<&'a T, Output = T>,
37    T: Sub<T, Output = T>,
38{
39    lhs[0] * rhs[0] - lhs[1] * rhs[1] - lhs[2] * rhs[2] - lhs[3] * rhs[3]
40}
41
42fn boost_factor<'a, T>(gamma: &'a T, square: impl FnOnce(&T) -> T) -> T
43where
44    &'a T: Add<f64, Output = T>,
45    T: Div<T, Output = T>,
46{
47    square(gamma) / (gamma + 1.0)
48}
49
50/// A vector with three components.
51///
52/// # Examples
53/// ```rust
54/// use laddu_physics::vectors::RealVec3;
55///
56/// let cross = RealVec3::x().cross(&RealVec3::y());
57/// assert_eq!(cross, RealVec3::z());
58/// ```
59#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct RealVec3 {
61    /// The x-component of the vector
62    pub x: f64,
63    /// The y-component of the vector
64    pub y: f64,
65    /// The z-component of the vector
66    pub z: f64,
67}
68
69impl Display for RealVec3 {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        write!(f, "[{:6.3}, {:6.3}, {:6.3}]", self.x, self.y, self.z)
72    }
73}
74
75impl AbsDiffEq for RealVec3 {
76    type Epsilon = <f64 as approx::AbsDiffEq>::Epsilon;
77
78    fn default_epsilon() -> Self::Epsilon {
79        f64::default_epsilon()
80    }
81
82    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
83        f64::abs_diff_eq(&self.x, &other.x, epsilon)
84            && f64::abs_diff_eq(&self.y, &other.y, epsilon)
85            && f64::abs_diff_eq(&self.z, &other.z, epsilon)
86    }
87}
88impl RelativeEq for RealVec3 {
89    fn default_max_relative() -> Self::Epsilon {
90        f64::default_max_relative()
91    }
92
93    fn relative_eq(
94        &self,
95        other: &Self,
96        epsilon: Self::Epsilon,
97        max_relative: Self::Epsilon,
98    ) -> bool {
99        f64::relative_eq(&self.x, &other.x, epsilon, max_relative)
100            && f64::relative_eq(&self.y, &other.y, epsilon, max_relative)
101            && f64::relative_eq(&self.z, &other.z, epsilon, max_relative)
102    }
103}
104
105impl From<RealVec3> for Vector3<f64> {
106    fn from(value: RealVec3) -> Self {
107        Vector3::new(value.x, value.y, value.z)
108    }
109}
110
111impl From<Vector3<f64>> for RealVec3 {
112    fn from(value: Vector3<f64>) -> Self {
113        RealVec3::new(value.x, value.y, value.z)
114    }
115}
116
117impl TryFrom<Vec<f64>> for RealVec3 {
118    type Error = LadduPhysicsError;
119
120    fn try_from(value: Vec<f64>) -> Result<Self, Self::Error> {
121        if value.len() != 3 {
122            return Err(LadduPhysicsError::custom(
123                "Attempted to convert Vec<f64> to RealVec3 for Vec with len != 3",
124            ));
125        }
126        Ok(Self {
127            x: value[0],
128            y: value[1],
129            z: value[2],
130        })
131    }
132}
133
134impl From<RealVec3> for Vec<f64> {
135    fn from(value: RealVec3) -> Self {
136        vec![value.x, value.y, value.z]
137    }
138}
139
140impl From<[f64; 3]> for RealVec3 {
141    fn from(value: [f64; 3]) -> Self {
142        Self {
143            x: value[0],
144            y: value[1],
145            z: value[2],
146        }
147    }
148}
149
150impl From<RealVec3> for [f64; 3] {
151    fn from(value: RealVec3) -> Self {
152        [value.x, value.y, value.z]
153    }
154}
155
156impl Default for RealVec3 {
157    fn default() -> Self {
158        RealVec3::zero()
159    }
160}
161
162impl RealVec3 {
163    /// Create a new 3-vector from its components
164    pub fn new(x: f64, y: f64, z: f64) -> Self {
165        RealVec3 { x, y, z }
166    }
167
168    /// Create a zero vector
169    pub const fn zero() -> Self {
170        RealVec3 {
171            x: 0.0,
172            y: 0.0,
173            z: 0.0,
174        }
175    }
176
177    /// Create a unit vector pointing in the x-direction
178    pub const fn x() -> Self {
179        RealVec3 {
180            x: 1.0,
181            y: 0.0,
182            z: 0.0,
183        }
184    }
185
186    /// Create a unit vector pointing in the y-direction
187    pub const fn y() -> Self {
188        RealVec3 {
189            x: 0.0,
190            y: 1.0,
191            z: 0.0,
192        }
193    }
194
195    /// Create a unit vector pointing in the z-direction
196    pub const fn z() -> Self {
197        RealVec3 {
198            x: 0.0,
199            y: 0.0,
200            z: 1.0,
201        }
202    }
203
204    /// Momentum in the x-direction
205    pub fn px(&self) -> f64 {
206        self.x
207    }
208
209    /// Momentum in the y-direction
210    pub fn py(&self) -> f64 {
211        self.y
212    }
213
214    /// Momentum in the z-direction
215    pub fn pz(&self) -> f64 {
216        self.z
217    }
218
219    /// Create a [`RealVec4`] with this vector as the 3-momentum and the given mass
220    pub fn with_mass(&self, mass: f64) -> RealVec4 {
221        let e = f64::sqrt(mass.powi(2) + self.mag2());
222        RealVec4::new(e, self.px(), self.py(), self.pz())
223    }
224
225    /// Create a [`RealVec4`] with this vector as the 3-momentum and the given energy
226    pub fn with_energy(&self, energy: f64) -> RealVec4 {
227        RealVec4::new(energy, self.px(), self.py(), self.pz())
228    }
229
230    /// Compute the dot product of this [`RealVec3`] and another
231    pub fn dot(&self, other: &RealVec3) -> f64 {
232        dot3([&self.x, &self.y, &self.z], [&other.x, &other.y, &other.z])
233    }
234
235    /// Compute the cross product of this [`RealVec3`] and another
236    pub fn cross(&self, other: &RealVec3) -> RealVec3 {
237        cross3([&self.x, &self.y, &self.z], [&other.x, &other.y, &other.z]).into()
238    }
239
240    /// The magnitude of the vector
241    pub fn mag(&self) -> f64 {
242        f64::sqrt(self.mag2())
243    }
244
245    /// The squared magnitude of the vector
246    pub fn mag2(&self) -> f64 {
247        self.dot(self)
248    }
249
250    /// The cosine of the polar angle $`\theta`$
251    ///
252    /// # Errors
253    ///
254    /// Returns [`LadduPhysicsError`] when the vector has zero or invalid
255    /// magnitude.
256    pub fn costheta(&self) -> LadduPhysicsResult<f64> {
257        let mag = self.mag();
258        if mag <= 0.0 {
259            return Err(LadduPhysicsError::invalid_value(
260                "vector magnitude",
261                "positive when calculating cos(theta)",
262                mag,
263            ));
264        }
265        Ok(self.z / self.mag())
266    }
267
268    /// The polar angle $`\theta`$
269    ///
270    /// # Errors
271    ///
272    /// Returns [`LadduPhysicsError`] when the vector has zero or invalid
273    /// magnitude.
274    pub fn theta(&self) -> LadduPhysicsResult<f64> {
275        Ok(f64::acos(self.costheta()?))
276    }
277
278    /// The azimuthal angle $`\phi`$
279    pub fn phi(&self) -> f64 {
280        f64::atan2(self.y, self.x)
281    }
282
283    /// Create a unit vector in the same direction as this [`RealVec3`]
284    ///
285    /// # Errors
286    ///
287    /// Returns [`LadduPhysicsError`] when the vector has zero or invalid
288    /// magnitude.
289    pub fn unit(&self) -> LadduPhysicsResult<RealVec3> {
290        let mag = self.mag();
291        if mag <= 0.0 {
292            return Err(LadduPhysicsError::invalid_value(
293                "vector magnitude",
294                "positive when constructing unit vector",
295                mag,
296            ));
297        }
298        Ok(RealVec3::new(self.x / mag, self.y / mag, self.z / mag))
299    }
300}
301
302impl<'a> std::iter::Sum<&'a RealVec3> for RealVec3 {
303    fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
304        iter.fold(Self::zero(), |a, b| a + b)
305    }
306}
307impl std::iter::Sum<RealVec3> for RealVec3 {
308    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
309        iter.fold(Self::zero(), |a, b| a + b)
310    }
311}
312
313impl_op_ex!(+ |a: &RealVec3, b: &RealVec3| -> RealVec3 { RealVec3::new(a.x + b.x, a.y + b.y, a.z + b.z) });
314impl_op_ex!(-|a: &RealVec3, b: &RealVec3| -> RealVec3 {
315    RealVec3::new(a.x - b.x, a.y - b.y, a.z - b.z)
316});
317impl_op_ex!(-|a: &RealVec3| -> RealVec3 { RealVec3::new(-a.x, -a.y, -a.z) });
318impl_op_ex_commutative!(+ |a: &RealVec3, b: &f64| -> RealVec3 { RealVec3::new(a.x + b, a.y + b, a.z + b) });
319impl_op_ex_commutative!(-|a: &RealVec3, b: &f64| -> RealVec3 {
320    RealVec3::new(a.x - b, a.y - b, a.z - b)
321});
322impl_op_ex_commutative!(*|a: &RealVec3, b: &f64| -> RealVec3 {
323    RealVec3::new(a.x * b, a.y * b, a.z * b)
324});
325impl_op_ex!(/ |a: &RealVec3, b: &f64| -> RealVec3 { RealVec3::new(a.x / b, a.y / b, a.z / b) });
326
327/// A four-vector (Lorentz vector) stored in `(E, p_x, p_y, p_z)` order.
328///
329/// # Examples
330/// ```rust
331/// use laddu_physics::vectors::{RealVec3, RealVec4};
332///
333/// let momentum = RealVec3::new(1.0, 0.0, 0.0);
334/// let four_vector = momentum.with_mass(2.0);
335/// assert!((four_vector.m2() - 4.0).abs() < 1e-12);
336/// ```
337#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
338pub struct RealVec4 {
339    /// Energy component.
340    pub e: f64,
341    /// Momentum in the x direction.
342    pub px: f64,
343    /// Momentum in the y direction.
344    pub py: f64,
345    /// Momentum in the z direction.
346    pub pz: f64,
347}
348
349impl Display for RealVec4 {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        write!(
352            f,
353            "[{:6.3}; {:6.3}, {:6.3}, {:6.3}]",
354            self.e, self.px, self.py, self.pz
355        )
356    }
357}
358
359impl AbsDiffEq for RealVec4 {
360    type Epsilon = <f64 as approx::AbsDiffEq>::Epsilon;
361
362    fn default_epsilon() -> Self::Epsilon {
363        f64::default_epsilon()
364    }
365
366    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
367        f64::abs_diff_eq(&self.e, &other.e, epsilon)
368            && f64::abs_diff_eq(&self.px, &other.px, epsilon)
369            && f64::abs_diff_eq(&self.py, &other.py, epsilon)
370            && f64::abs_diff_eq(&self.pz, &other.pz, epsilon)
371    }
372}
373impl RelativeEq for RealVec4 {
374    fn default_max_relative() -> Self::Epsilon {
375        f64::default_max_relative()
376    }
377
378    fn relative_eq(
379        &self,
380        other: &Self,
381        epsilon: Self::Epsilon,
382        max_relative: Self::Epsilon,
383    ) -> bool {
384        f64::relative_eq(&self.e, &other.e, epsilon, max_relative)
385            && f64::relative_eq(&self.px, &other.px, epsilon, max_relative)
386            && f64::relative_eq(&self.py, &other.py, epsilon, max_relative)
387            && f64::relative_eq(&self.pz, &other.pz, epsilon, max_relative)
388    }
389}
390
391impl From<RealVec4> for Vector4<f64> {
392    fn from(value: RealVec4) -> Self {
393        let [e, px, py, pz] = value.components();
394        Vector4::new(e, px, py, pz)
395    }
396}
397
398impl From<Vector4<f64>> for RealVec4 {
399    fn from(value: Vector4<f64>) -> Self {
400        RealVec4::new(value.x, value.y, value.z, value.w)
401    }
402}
403
404impl TryFrom<Vec<f64>> for RealVec4 {
405    type Error = LadduPhysicsError;
406
407    fn try_from(value: Vec<f64>) -> Result<Self, Self::Error> {
408        if value.len() != 4 {
409            return Err(LadduPhysicsError::custom(
410                "Attempted to convert Vec<f64> to RealVec4 for Vec with len != 4",
411            ));
412        }
413        Ok(Self {
414            e: value[0],
415            px: value[1],
416            py: value[2],
417            pz: value[3],
418        })
419    }
420}
421
422impl From<RealVec4> for Vec<f64> {
423    fn from(value: RealVec4) -> Self {
424        Vec::from(value.components())
425    }
426}
427
428impl From<[f64; 4]> for RealVec4 {
429    fn from(value: [f64; 4]) -> Self {
430        Self {
431            e: value[0],
432            px: value[1],
433            py: value[2],
434            pz: value[3],
435        }
436    }
437}
438
439impl From<RealVec4> for [f64; 4] {
440    fn from(value: RealVec4) -> Self {
441        value.components()
442    }
443}
444
445impl RealVec4 {
446    /// Create a four-vector in metric order `(E, p_x, p_y, p_z)`.
447    pub fn new(e: f64, px: f64, py: f64, pz: f64) -> Self {
448        RealVec4 { e, px, py, pz }
449    }
450
451    /// Return components in the canonical storage order `(E, p_x, p_y, p_z)`.
452    ///
453    /// The returned array can be iterated directly when encoding external
454    /// columns without duplicating the component-order policy.
455    pub const fn components(&self) -> [f64; 4] {
456        [self.e, self.px, self.py, self.pz]
457    }
458
459    /// Momentum in the x-direction
460    pub fn px(&self) -> f64 {
461        self.px
462    }
463
464    /// Momentum in the y-direction
465    pub fn py(&self) -> f64 {
466        self.py
467    }
468
469    /// Momentum in the z-direction
470    pub fn pz(&self) -> f64 {
471        self.pz
472    }
473
474    /// The energy of the 4-vector
475    pub fn e(&self) -> f64 {
476        self.e
477    }
478
479    /// The 3-momentum
480    pub fn momentum(&self) -> RealVec3 {
481        self.vec3()
482    }
483
484    /// The $`\gamma`$ factor $`\frac{1}{\sqrt{1 - \beta^2}}`$.
485    ///
486    /// # Errors
487    ///
488    /// Returns [`LadduPhysicsError`] when the energy is not positive or the
489    /// resulting velocity is not subluminal.
490    pub fn gamma(&self) -> LadduPhysicsResult<f64> {
491        let beta = self.beta()?;
492        let b2 = beta.dot(&beta);
493        if b2 >= 1.0 {
494            return Err(LadduPhysicsError::invalid_value("|beta|^2", "< 1", b2));
495        }
496        Ok(1.0 / f64::sqrt(1.0 - b2))
497    }
498
499    /// The $`\vec{\beta}`$ vector $`\frac{\vec{p}}{E}`$.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`LadduPhysicsError`] when the four-momentum energy is not
504    /// positive.
505    pub fn beta(&self) -> LadduPhysicsResult<RealVec3> {
506        let e = self.e();
507        if e <= 0.0 {
508            return Err(LadduPhysicsError::invalid_value(
509                "four-momentum energy",
510                "positive",
511                e,
512            ));
513        }
514        Ok(self.momentum() / e)
515    }
516
517    /// The invariant mass corresponding to this 4-momentum
518    ///
519    /// # Errors
520    ///
521    /// Returns [`LadduPhysicsError`] when the invariant mass squared is
522    /// non-finite or negative.
523    pub fn m(&self) -> LadduPhysicsResult<f64> {
524        self.mag()
525    }
526
527    #[inline(always)]
528    /// Return the invariant mass without checking for a spacelike vector.
529    pub fn m_unchecked(&self) -> f64 {
530        self.m2().sqrt()
531    }
532
533    /// Return the signed invariant mass.
534    ///
535    /// # Errors
536    ///
537    /// Returns [`LadduPhysicsError`] when the invariant mass squared is
538    /// non-finite.
539    pub fn signed_m(&self) -> LadduPhysicsResult<f64> {
540        self.signed_mag()
541    }
542
543    #[inline(always)]
544    /// Return the signed invariant mass without checking for finite components.
545    pub fn signed_m_unchecked(&self) -> f64 {
546        self.signed_mag_unchecked()
547    }
548
549    /// The squared invariant mass corresponding to this 4-momentum
550    pub fn m2(&self) -> f64 {
551        self.mag2()
552    }
553
554    /// Compute the Lorentz inner product with another four-vector.
555    pub fn dot(&self, other: &Self) -> f64 {
556        lorentz_dot4(
557            [&self.e, &self.px, &self.py, &self.pz],
558            [&other.e, &other.px, &other.py, &other.pz],
559        )
560    }
561
562    /// Pretty-prints the four-momentum.
563    pub fn to_p4_string(&self) -> String {
564        let mass = self
565            .m()
566            .map(|m| format!("{m:.5}"))
567            .unwrap_or_else(|_| format!("{:.5}i", (-self.m2()).sqrt()));
568        format!(
569            "[e = {:.5}; p = ({:.5}, {:.5}, {:.5}); m = {}]",
570            self.e(),
571            self.px(),
572            self.py(),
573            self.pz(),
574            mass
575        )
576    }
577
578    /// Alias for [`Self::m`] using the $`+---`$ metric.
579    ///
580    /// # Errors
581    ///
582    /// Returns [`LadduPhysicsError`] for non-finite or spacelike four-vectors,
583    /// where `m2 < 0`.
584    pub fn mag(&self) -> LadduPhysicsResult<f64> {
585        let mag2 = self.mag2();
586
587        if !mag2.is_finite() {
588            return Err(LadduPhysicsError::invalid_value(
589                "magnitude squared",
590                "finite",
591                mag2,
592            ));
593        }
594
595        if mag2 < 0.0 {
596            return Err(LadduPhysicsError::invalid_value(
597                "magnitude squared",
598                "nonnegative",
599                mag2,
600            ));
601        }
602
603        Ok(mag2.sqrt())
604    }
605
606    /// Signed invariant mass useful for diagnostics:
607    ///
608    /// - `sqrt(mag2)` for timelike/null vectors
609    /// - `-sqrt(-mag2)` for spacelike vectors
610    ///
611    /// # Errors
612    ///
613    /// Returns [`LadduPhysicsError`] when the invariant magnitude squared is
614    /// non-finite.
615    pub fn signed_mag(&self) -> LadduPhysicsResult<f64> {
616        let mag2 = self.mag2();
617
618        if !mag2.is_finite() {
619            return Err(LadduPhysicsError::invalid_value(
620                "magnitude squared",
621                "finite",
622                mag2,
623            ));
624        }
625
626        if mag2 >= 0.0 {
627            Ok(mag2.sqrt())
628        } else {
629            Ok(-(-mag2).sqrt())
630        }
631    }
632
633    #[inline(always)]
634    /// Return the signed magnitude without checking for finite components.
635    pub fn signed_mag_unchecked(&self) -> f64 {
636        let mag2 = self.mag2();
637        if mag2 >= 0.0 {
638            mag2.sqrt()
639        } else {
640            -(-mag2).sqrt()
641        }
642    }
643
644    /// Alias for [`Self::m2`], the squared invariant mass in the $`+---`$ metric.
645    pub fn mag2(&self) -> f64 {
646        self.e * self.e - (self.px * self.px + self.py * self.py + self.pz * self.pz)
647    }
648
649    /// Gives the vector boosted along a $`\vec{\beta}`$ vector.
650    pub fn boost(&self, beta: &RealVec3) -> Self {
651        let b2 = beta.dot(beta);
652        if b2 == 0.0 {
653            return *self;
654        }
655        let gamma = 1.0 / f64::sqrt(1.0 - b2);
656        let factor = boost_factor(&gamma, |gamma| gamma * gamma);
657        let p3 = self.vec3() + beta * (factor * self.vec3().dot(beta) + gamma * self.e);
658        RealVec4::new(gamma * (self.e + beta.dot(&self.vec3())), p3.x, p3.y, p3.z)
659    }
660
661    /// The 3-vector contained in this 4-vector
662    pub fn vec3(&self) -> RealVec3 {
663        RealVec3 {
664            x: self.px,
665            y: self.py,
666            z: self.pz,
667        }
668    }
669}
670
671impl_op_ex!(+ |a: &RealVec4, b: &RealVec4| -> RealVec4 { RealVec4::new(a.e + b.e, a.px + b.px, a.py + b.py, a.pz + b.pz) });
672impl_op_ex!(-|a: &RealVec4, b: &RealVec4| -> RealVec4 {
673    RealVec4::new(a.e - b.e, a.px - b.px, a.py - b.py, a.pz - b.pz)
674});
675impl_op_ex!(-|a: &RealVec4| -> RealVec4 { RealVec4::new(a.e, -a.px, -a.py, -a.pz) });
676
677impl<'a> std::iter::Sum<&'a RealVec4> for RealVec4 {
678    fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
679        iter.fold(Self::new(0.0, 0.0, 0.0, 0.0), |a, b| a + b)
680    }
681}
682
683impl std::iter::Sum<RealVec4> for RealVec4 {
684    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
685        iter.fold(Self::new(0.0, 0.0, 0.0, 0.0), |a, b| a + b)
686    }
687}
688
689/// Expression-valued vector with three spatial components.
690#[derive(Clone, Debug, Serialize, Deserialize)]
691pub struct Vec3 {
692    /// Symbolic x component.
693    pub x: Expr,
694    /// Symbolic y component.
695    pub y: Expr,
696    /// Symbolic z component.
697    pub z: Expr,
698}
699
700impl Vec3 {
701    /// Construct a symbolic Cartesian three-vector.
702    pub fn new(x: impl Into<Expr>, y: impl Into<Expr>, z: impl Into<Expr>) -> Self {
703        Self {
704            x: x.into(),
705            y: y.into(),
706            z: z.into(),
707        }
708    }
709
710    /// Return the symbolic zero vector.
711    pub fn zero() -> Self {
712        Self::new(0.0, 0.0, 0.0)
713    }
714
715    /// Return the positive x-axis unit vector.
716    pub fn x() -> Self {
717        Self::new(1.0, 0.0, 0.0)
718    }
719
720    /// Return the positive y-axis unit vector.
721    pub fn y() -> Self {
722        Self::new(0.0, 1.0, 0.0)
723    }
724
725    /// Return the positive z-axis unit vector.
726    pub fn z() -> Self {
727        Self::new(0.0, 0.0, 1.0)
728    }
729
730    /// Read the spatial components of a named event four-vector.
731    pub fn event(prefix: &str) -> Self {
732        Self::new(
733            event_p4_component(prefix, P4Component::Px),
734            event_p4_component(prefix, P4Component::Py),
735            event_p4_component(prefix, P4Component::Pz),
736        )
737    }
738
739    /// Return the x momentum component.
740    pub fn px(&self) -> Expr {
741        self.x.clone()
742    }
743
744    /// Return the y momentum component.
745    pub fn py(&self) -> Expr {
746        self.y.clone()
747    }
748
749    /// Return the z momentum component.
750    pub fn pz(&self) -> Expr {
751        self.z.clone()
752    }
753
754    /// Compute the Euclidean inner product.
755    pub fn dot(&self, other: &Self) -> Expr {
756        dot3([&self.x, &self.y, &self.z], [&other.x, &other.y, &other.z])
757    }
758
759    /// Compute the Cartesian cross product.
760    pub fn cross(&self, other: &Self) -> Self {
761        let [x, y, z] = cross3([&self.x, &self.y, &self.z], [&other.x, &other.y, &other.z]);
762        Self::new(x, y, z)
763    }
764
765    /// Return the squared Euclidean magnitude.
766    pub fn mag2(&self) -> Expr {
767        self.dot(self)
768    }
769
770    /// Return the Euclidean magnitude.
771    pub fn mag(&self) -> Expr {
772        self.mag2().sqrt()
773    }
774
775    /// Return the cosine of the polar angle.
776    pub fn costheta(&self) -> Expr {
777        &self.z / self.mag()
778    }
779
780    /// Return a vector normalized to unit magnitude.
781    pub fn unit(&self) -> Self {
782        self / &self.mag()
783    }
784
785    /// Return the azimuthal angle.
786    pub fn phi(&self) -> Expr {
787        atan2(self.py(), self.px())
788    }
789
790    /// Promote this momentum to a four-vector with the given invariant mass.
791    pub fn with_mass(&self, mass: impl Into<Expr>) -> Vec4 {
792        let mass = mass.into();
793        Vec4::new(
794            (mass.powi(2) + self.mag2()).sqrt(),
795            self.px(),
796            self.py(),
797            self.pz(),
798        )
799    }
800
801    /// Promote this momentum to a four-vector with the given energy.
802    pub fn with_energy(&self, energy: impl Into<Expr>) -> Vec4 {
803        Vec4::new(energy, self.px(), self.py(), self.pz())
804    }
805
806    /// Convert this vector to a vector-valued expression.
807    pub fn as_expr(&self) -> Expr {
808        vector([self.x.clone(), self.y.clone(), self.z.clone()])
809    }
810
811    fn scale(&self, scalar: impl Into<Expr>) -> Self {
812        let scalar = scalar.into();
813        Self::new(&self.x * &scalar, &self.y * &scalar, &self.z * scalar)
814    }
815}
816
817impl From<RealVec3> for Vec3 {
818    fn from(value: RealVec3) -> Self {
819        Self::new(value.x, value.y, value.z)
820    }
821}
822
823impl Default for Vec3 {
824    fn default() -> Self {
825        Self::zero()
826    }
827}
828
829impl_op_ex!(+ |a: &Vec3, b: &Vec3| -> Vec3 { Vec3::new(&a.x + &b.x, &a.y + &b.y, &a.z + &b.z) });
830impl_op_ex!(-|a: &Vec3, b: &Vec3| -> Vec3 { Vec3::new(&a.x - &b.x, &a.y - &b.y, &a.z - &b.z) });
831impl_op_ex!(-|a: &Vec3| -> Vec3 { Vec3::new(-&a.x, -&a.y, -&a.z) });
832impl_op_ex!(*|a: &Vec3, b: &Expr| -> Vec3 { a.scale(b) });
833impl_op_ex!(*|a: &Expr, b: &Vec3| -> Vec3 { b.scale(a) });
834impl_op_ex!(*|a: &Vec3, b: &f64| -> Vec3 { a.scale(b) });
835impl_op_ex!(*|a: &f64, b: &Vec3| -> Vec3 { b.scale(a) });
836impl_op_ex!(/ |a: &Vec3, b: &Expr| -> Vec3 {
837    Vec3::new(&a.x / b, &a.y / b, &a.z / b)
838});
839impl_op_ex!(/ |a: &Vec3, b: &f64| -> Vec3 {
840    Vec3::new(&a.x / b, &a.y / b, &a.z / b)
841});
842
843/// Expression-valued four-vector in `(E, p_x, p_y, p_z)` order with a `+---` metric.
844#[derive(Clone, Debug, Serialize, Deserialize)]
845pub struct Vec4 {
846    /// Energy component.
847    pub e: Expr,
848    /// Momentum in the x direction.
849    pub px: Expr,
850    /// Momentum in the y direction.
851    pub py: Expr,
852    /// Momentum in the z direction.
853    pub pz: Expr,
854}
855
856impl Vec4 {
857    /// Create a symbolic four-vector in metric order `(E, p_x, p_y, p_z)`.
858    pub fn new(
859        e: impl Into<Expr>,
860        px: impl Into<Expr>,
861        py: impl Into<Expr>,
862        pz: impl Into<Expr>,
863    ) -> Self {
864        Self {
865            e: e.into(),
866            px: px.into(),
867            py: py.into(),
868            pz: pz.into(),
869        }
870    }
871
872    /// Read a named event four-vector.
873    pub fn event(prefix: &str) -> Self {
874        Self::new(
875            event_p4_component(prefix, P4Component::E),
876            event_p4_component(prefix, P4Component::Px),
877            event_p4_component(prefix, P4Component::Py),
878            event_p4_component(prefix, P4Component::Pz),
879        )
880    }
881
882    /// Return the x momentum expression.
883    pub fn px(&self) -> Expr {
884        self.px.clone()
885    }
886
887    /// Return the y momentum expression.
888    pub fn py(&self) -> Expr {
889        self.py.clone()
890    }
891
892    /// Return the z momentum expression.
893    pub fn pz(&self) -> Expr {
894        self.pz.clone()
895    }
896
897    /// Return the energy expression.
898    pub fn e(&self) -> Expr {
899        self.e.clone()
900    }
901
902    /// Return the spatial momentum.
903    pub fn momentum(&self) -> Vec3 {
904        self.vec3()
905    }
906
907    /// Return the spatial three-vector.
908    pub fn vec3(&self) -> Vec3 {
909        Vec3::new(self.px(), self.py(), self.pz())
910    }
911
912    /// Return the three-velocity `p / E`.
913    pub fn beta(&self) -> Vec3 {
914        self.momentum() / self.e()
915    }
916
917    /// Return the Lorentz factor.
918    pub fn gamma(&self) -> Expr {
919        1.0 / (1.0 - self.beta().mag2()).sqrt()
920    }
921
922    /// Return the squared invariant mass.
923    pub fn m2(&self) -> Expr {
924        self.mag2()
925    }
926
927    /// Return the invariant mass.
928    pub fn m(&self) -> Expr {
929        self.mag()
930    }
931
932    /// Compute the Lorentz inner product with another symbolic four-vector.
933    pub fn dot(&self, other: &Self) -> Expr {
934        lorentz_dot4(
935            [&self.e, &self.px, &self.py, &self.pz],
936            [&other.e, &other.px, &other.py, &other.pz],
937        )
938    }
939
940    /// Alias for [`Self::m2`], the squared invariant mass.
941    pub fn mag2(&self) -> Expr {
942        self.dot(self)
943    }
944
945    /// Alias for [`Self::m`], the invariant mass.
946    pub fn mag(&self) -> Expr {
947        self.mag2().sqrt()
948    }
949
950    /// Apply a Lorentz boost by a three-velocity.
951    pub fn boost(&self, beta: &Vec3) -> Self {
952        let b2 = beta.dot(beta);
953        let gamma = (1.0 - &b2).sqrt();
954        let gamma = 1.0 / gamma;
955        let factor = boost_factor(&gamma, |gamma| gamma.powi(2));
956        let p3 = self.vec3() + beta * ((factor * self.vec3().dot(beta)) + &gamma * &self.e);
957        Self::new(gamma * (&self.e + beta.dot(&self.vec3())), p3.x, p3.y, p3.z)
958    }
959
960    /// Convert this four-vector to a vector-valued expression.
961    pub fn as_expr(&self) -> Expr {
962        vector([
963            self.e.clone(),
964            self.px.clone(),
965            self.py.clone(),
966            self.pz.clone(),
967        ])
968    }
969}
970
971impl From<RealVec4> for Vec4 {
972    fn from(value: RealVec4) -> Self {
973        Self::new(value.e, value.px, value.py, value.pz)
974    }
975}
976
977impl_op_ex!(+ |a: &Vec4, b: &Vec4| -> Vec4 {
978    Vec4::new(&a.e + &b.e, &a.px + &b.px, &a.py + &b.py, &a.pz + &b.pz)
979});
980impl_op_ex!(-|a: &Vec4, b: &Vec4| -> Vec4 {
981    Vec4::new(&a.e - &b.e, &a.px - &b.px, &a.py - &b.py, &a.pz - &b.pz)
982});
983impl_op_ex!(-|a: &Vec4| -> Vec4 { Vec4::new(-&a.e, -&a.px, -&a.py, -&a.pz) });
984
985#[cfg(test)]
986mod tests {
987    use approx::{assert_abs_diff_eq, assert_relative_eq};
988    use fastrand::Rng;
989    use laddu_compile::CompiledModel;
990    use laddu_runtime::CpuBackend;
991    use nalgebra::{Vector3, Vector4};
992    use num::complex::Complex64;
993
994    use super::*;
995
996    fn evaluate(expr: laddu_expr::Expr) -> Complex64 {
997        let model = CompiledModel::from_expr(&expr).unwrap();
998        let params = model.params().default_values();
999        CpuBackend.prepare(&model).evaluate(&params).unwrap()
1000    }
1001
1002    fn evaluate_real(expr: Expr) -> f64 {
1003        evaluate(expr).re
1004    }
1005
1006    fn assert_symbolic_vec4_eq(symbolic: Vec4, real: RealVec4, epsilon: f64) {
1007        assert_relative_eq!(evaluate_real(symbolic.e), real.e, epsilon = epsilon);
1008        assert_relative_eq!(evaluate_real(symbolic.px), real.px, epsilon = epsilon);
1009        assert_relative_eq!(evaluate_real(symbolic.py), real.py, epsilon = epsilon);
1010        assert_relative_eq!(evaluate_real(symbolic.pz), real.pz, epsilon = epsilon);
1011    }
1012
1013    #[test]
1014    fn test_display() {
1015        let v3 = RealVec3::new(1.2341, -2.3452, 3.4563);
1016        assert_eq!(format!("{}", v3), "[ 1.234, -2.345,  3.456]");
1017        let v4 = RealVec4::new(4.5674, 1.2341, -2.3452, 3.4563);
1018        assert_eq!(format!("{}", v4), "[ 4.567;  1.234, -2.345,  3.456]");
1019    }
1020
1021    #[test]
1022    fn test_vec_vector_conversion() {
1023        let v = RealVec3::new(1.0, 2.0, 3.0);
1024        let vector3: Vec<f64> = v.into();
1025        assert_eq!(vector3[0], 1.0);
1026        assert_eq!(vector3[1], 2.0);
1027        assert_eq!(vector3[2], 3.0);
1028
1029        let v_from_vec: RealVec3 = vector3.try_into().unwrap();
1030        assert_eq!(v_from_vec, v);
1031
1032        let v = RealVec4::new(1.0, 2.0, 3.0, 4.0);
1033        let vector4: Vec<f64> = v.into();
1034        assert_eq!(vector4[0], 1.0);
1035        assert_eq!(vector4[1], 2.0);
1036        assert_eq!(vector4[2], 3.0);
1037        assert_eq!(vector4[3], 4.0);
1038
1039        let v_from_vec: RealVec4 = vector4.try_into().unwrap();
1040        assert_eq!(v_from_vec, v);
1041    }
1042
1043    #[test]
1044    fn test_vec_array_conversion() {
1045        let arr = [1.0, 2.0, 3.0];
1046        let v: RealVec3 = arr.into();
1047        assert_eq!(v, RealVec3::new(1.0, 2.0, 3.0));
1048
1049        let back_to_array: [f64; 3] = v.into();
1050        assert_eq!(back_to_array, arr);
1051
1052        let arr = [1.0, 2.0, 3.0, 4.0];
1053        let v: RealVec4 = arr.into();
1054        assert_eq!(v, RealVec4::new(1.0, 2.0, 3.0, 4.0));
1055
1056        let back_to_array: [f64; 4] = v.into();
1057        assert_eq!(back_to_array, arr);
1058        assert_eq!(v.components(), arr);
1059    }
1060
1061    #[test]
1062    fn test_vec_nalgebra_conversion() {
1063        let v = RealVec3::new(1.0, 2.0, 3.0);
1064        let vector3: Vector3<f64> = v.into();
1065        assert_eq!(vector3.x, 1.0);
1066        assert_eq!(vector3.y, 2.0);
1067        assert_eq!(vector3.z, 3.0);
1068
1069        let v_from_vec: RealVec3 = vector3.into();
1070        assert_eq!(v_from_vec, v);
1071
1072        let v = RealVec4::new(1.0, 2.0, 3.0, 4.0);
1073        let vector4: Vector4<f64> = v.into();
1074        assert_eq!(vector4.x, 1.0);
1075        assert_eq!(vector4.y, 2.0);
1076        assert_eq!(vector4.z, 3.0);
1077        assert_eq!(vector4.w, 4.0);
1078
1079        let v_from_vec: RealVec4 = vector4.into();
1080        assert_eq!(v_from_vec, v);
1081    }
1082
1083    #[test]
1084    fn test_vec_sums() {
1085        let vectors = [RealVec3::new(1.0, 2.0, 3.0), RealVec3::new(4.0, 5.0, 6.0)];
1086        let sum: RealVec3 = vectors.iter().sum();
1087        assert_eq!(sum, RealVec3::new(5.0, 7.0, 9.0));
1088        let sum: RealVec3 = vectors.into_iter().sum();
1089        assert_eq!(sum, RealVec3::new(5.0, 7.0, 9.0));
1090
1091        let vectors = [
1092            RealVec4::new(1.0, 2.0, 3.0, 4.0),
1093            RealVec4::new(4.0, 5.0, 6.0, 7.0),
1094        ];
1095        let sum: RealVec4 = vectors.iter().sum();
1096        assert_eq!(sum, RealVec4::new(5.0, 7.0, 9.0, 11.0));
1097        let sum: RealVec4 = vectors.into_iter().sum();
1098        assert_eq!(sum, RealVec4::new(5.0, 7.0, 9.0, 11.0));
1099    }
1100
1101    #[test]
1102    fn test_three_to_four_momentum_conversion() {
1103        let p3 = RealVec3::new(1.0, 2.0, 3.0);
1104        let target_p4 = RealVec4::new(10.0, 1.0, 2.0, 3.0);
1105        let p4_from_mass = p3.with_mass(target_p4.m().unwrap());
1106        assert_eq!(target_p4.e(), p4_from_mass.e());
1107        assert_eq!(target_p4.px(), p4_from_mass.px());
1108        assert_eq!(target_p4.py(), p4_from_mass.py());
1109        assert_eq!(target_p4.pz(), p4_from_mass.pz());
1110        let p4_from_energy = p3.with_energy(target_p4.e());
1111        assert_eq!(target_p4.e(), p4_from_energy.e());
1112        assert_eq!(target_p4.px(), p4_from_energy.px());
1113        assert_eq!(target_p4.py(), p4_from_energy.py());
1114        assert_eq!(target_p4.pz(), p4_from_energy.pz());
1115    }
1116
1117    #[test]
1118    fn test_four_momentum_basics() {
1119        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1120        assert_eq!(p.e(), 10.0);
1121        assert_eq!(p.px(), 3.0);
1122        assert_eq!(p.py(), 4.0);
1123        assert_eq!(p.pz(), 5.0);
1124        assert_eq!(p.momentum().px(), 3.0);
1125        assert_eq!(p.momentum().py(), 4.0);
1126        assert_eq!(p.momentum().pz(), 5.0);
1127        assert_relative_eq!(p.beta().unwrap().x, 0.3);
1128        assert_relative_eq!(p.beta().unwrap().y, 0.4);
1129        assert_relative_eq!(p.beta().unwrap().z, 0.5);
1130        assert_relative_eq!(p.m2(), 50.0);
1131        assert_relative_eq!(p.m().unwrap(), f64::sqrt(50.0));
1132        assert_eq!(
1133            p.to_p4_string().to_string(),
1134            "[e = 10.00000; p = (3.00000, 4.00000, 5.00000); m = 7.07107]"
1135        );
1136        assert_relative_eq!(RealVec3::x().x, 1.0);
1137        assert_relative_eq!(RealVec3::x().y, 0.0);
1138        assert_relative_eq!(RealVec3::x().z, 0.0);
1139        assert_relative_eq!(RealVec3::y().x, 0.0);
1140        assert_relative_eq!(RealVec3::y().y, 1.0);
1141        assert_relative_eq!(RealVec3::y().z, 0.0);
1142        assert_relative_eq!(RealVec3::z().x, 0.0);
1143        assert_relative_eq!(RealVec3::z().y, 0.0);
1144        assert_relative_eq!(RealVec3::z().z, 1.0);
1145        assert_relative_eq!(RealVec3::default().x, 0.0);
1146        assert_relative_eq!(RealVec3::default().y, 0.0);
1147        assert_relative_eq!(RealVec3::default().z, 0.0);
1148    }
1149
1150    #[test]
1151    fn test_three_momentum_basics() {
1152        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1153        let q = RealVec4::new(0.0, 1.2, -3.4, 7.6);
1154        let p3_view = p.momentum();
1155        let q3_view = q.momentum();
1156        assert_eq!(p3_view.px(), 3.0);
1157        assert_eq!(p3_view.py(), 4.0);
1158        assert_eq!(p3_view.pz(), 5.0);
1159        assert_relative_eq!(p3_view.mag2(), 50.0);
1160        assert_relative_eq!(p3_view.mag(), f64::sqrt(50.0));
1161        assert_relative_eq!(p3_view.costheta().unwrap(), 5.0 / f64::sqrt(50.0));
1162        assert_relative_eq!(p3_view.theta().unwrap(), f64::acos(5.0 / f64::sqrt(50.0)));
1163        assert_relative_eq!(p3_view.phi(), f64::atan2(4.0, 3.0));
1164        assert_relative_eq!(
1165            p3_view.unit().unwrap(),
1166            RealVec3::new(
1167                3.0 / f64::sqrt(50.0),
1168                4.0 / f64::sqrt(50.0),
1169                5.0 / f64::sqrt(50.0)
1170            )
1171        );
1172        assert_relative_eq!(p3_view.cross(&q3_view), RealVec3::new(47.4, -16.8, -15.0));
1173    }
1174
1175    #[test]
1176    fn test_vec_equality() {
1177        let p = RealVec3::new(1.1, 2.2, 3.3);
1178        let p2 = RealVec3::new(1.1 * 2.0, 2.2 * 2.0, 3.3 * 2.0);
1179        assert_abs_diff_eq!(p * 2.0, p2);
1180        assert_relative_eq!(p * 2.0, p2);
1181    }
1182
1183    #[test]
1184    fn test_boost_com() {
1185        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1186        let zero = p.boost(&-p.beta().unwrap()).momentum();
1187        assert_relative_eq!(zero, RealVec3::zero());
1188    }
1189
1190    #[test]
1191    fn test_boost() {
1192        let p0 = RealVec4::new(1.0, 0.0, 0.0, 0.0);
1193        assert_relative_eq!(p0.gamma().unwrap(), 1.0);
1194        let p0 = RealVec4::new(1.0, f64::sqrt(3.0) / 2.0, 0.0, 0.0);
1195        assert_relative_eq!(p0.gamma().unwrap(), 2.0);
1196        let p1 = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1197        let p2 = RealVec4::new(9.0, 3.4, 2.3, 1.2);
1198        let p1_boosted = p1.boost(&-p2.beta().unwrap());
1199        assert_relative_eq!(p1_boosted.e(), 8.157632144622882);
1200        assert_relative_eq!(p1_boosted.px(), -0.6489200627053444);
1201        assert_relative_eq!(p1_boosted.py(), 1.5316128987581492);
1202        assert_relative_eq!(p1_boosted.pz(), 3.712145860221643);
1203    }
1204
1205    #[test]
1206    fn real_and_symbolic_vector_formulas_agree_on_finite_inputs() {
1207        let mut rng = Rng::with_seed(0x5645_4354_4f52);
1208
1209        for _ in 0..32 {
1210            let a3 = RealVec3::new(
1211                rng.f64() * 10.0 - 5.0,
1212                rng.f64() * 10.0 - 5.0,
1213                rng.f64() * 10.0 - 5.0,
1214            );
1215            let b3 = RealVec3::new(
1216                rng.f64() * 10.0 - 5.0,
1217                rng.f64() * 10.0 - 5.0,
1218                rng.f64() * 10.0 - 5.0,
1219            );
1220            let a = Vec3::from(a3);
1221            let b = Vec3::from(b3);
1222            assert_relative_eq!(evaluate_real(a.dot(&b)), a3.dot(&b3), epsilon = 1e-11);
1223            let symbolic_cross = a.cross(&b);
1224            let real_cross = a3.cross(&b3);
1225            assert_relative_eq!(
1226                evaluate_real(symbolic_cross.x),
1227                real_cross.x,
1228                epsilon = 1e-11
1229            );
1230            assert_relative_eq!(
1231                evaluate_real(symbolic_cross.y),
1232                real_cross.y,
1233                epsilon = 1e-11
1234            );
1235            assert_relative_eq!(
1236                evaluate_real(symbolic_cross.z),
1237                real_cross.z,
1238                epsilon = 1e-11
1239            );
1240
1241            let p = RealVec4::new(
1242                rng.f64() * 9.0 + 1.0,
1243                rng.f64() * 10.0 - 5.0,
1244                rng.f64() * 10.0 - 5.0,
1245                rng.f64() * 10.0 - 5.0,
1246            );
1247            let q = RealVec4::new(
1248                rng.f64() * 9.0 + 1.0,
1249                rng.f64() * 10.0 - 5.0,
1250                rng.f64() * 10.0 - 5.0,
1251                rng.f64() * 10.0 - 5.0,
1252            );
1253            let symbolic_p = Vec4::from(p);
1254            let symbolic_q = Vec4::from(q);
1255            assert_relative_eq!(
1256                evaluate_real(symbolic_p.dot(&symbolic_q)),
1257                p.dot(&q),
1258                epsilon = 1e-11
1259            );
1260            assert_relative_eq!(evaluate_real(symbolic_p.m2()), p.m2(), epsilon = 1e-11);
1261
1262            let beta = RealVec3::new(
1263                rng.f64() * 0.6 - 0.3,
1264                rng.f64() * 0.6 - 0.3,
1265                rng.f64() * 0.6 - 0.3,
1266            );
1267            assert_symbolic_vec4_eq(
1268                Vec4::from(p).boost(&Vec3::from(beta)),
1269                p.boost(&beta),
1270                1e-10,
1271            );
1272        }
1273    }
1274
1275    #[test]
1276    fn vector_formula_boundaries_remain_well_defined() {
1277        let p = RealVec4::new(2.0, 0.25, -0.5, 1.0);
1278        assert_eq!(p.boost(&RealVec3::zero()), p);
1279        assert_symbolic_vec4_eq(Vec4::from(p).boost(&Vec3::zero()), p, 1e-12);
1280
1281        let near_zero = RealVec3::new(1e-12, -1e-12, 1e-12);
1282        assert_symbolic_vec4_eq(
1283            Vec4::from(p).boost(&Vec3::from(near_zero)),
1284            p.boost(&near_zero),
1285            1e-12,
1286        );
1287
1288        let near_lightlike = RealVec4::new(1.0, 1.0 - 1e-12, 0.0, 0.0);
1289        assert_relative_eq!(
1290            evaluate_real(Vec4::from(near_lightlike).m2()),
1291            near_lightlike.m2(),
1292            epsilon = 1e-15
1293        );
1294
1295        let near_light_speed = RealVec3::new(1.0 - 1e-12, 0.0, 0.0);
1296        assert_symbolic_vec4_eq(
1297            Vec4::from(p).boost(&Vec3::from(near_light_speed)),
1298            p.boost(&near_light_speed),
1299            1e-6,
1300        );
1301    }
1302
1303    #[test]
1304    fn collinear_frame_composition_matches_combined_boost() {
1305        let p = RealVec4::new(5.0, 0.4, -0.2, 1.5);
1306        let first = 0.2;
1307        let second = -0.35;
1308        let combined = (first + second) / (1.0 + first * second);
1309
1310        let sequential = p
1311            .boost(&RealVec3::new(first, 0.0, 0.0))
1312            .boost(&RealVec3::new(second, 0.0, 0.0));
1313        let direct = p.boost(&RealVec3::new(combined, 0.0, 0.0));
1314        assert_relative_eq!(sequential, direct, epsilon = 1e-12);
1315        assert_symbolic_vec4_eq(
1316            Vec4::from(p)
1317                .boost(&Vec3::new(first, 0.0, 0.0))
1318                .boost(&Vec3::new(second, 0.0, 0.0)),
1319            sequential,
1320            1e-12,
1321        );
1322    }
1323
1324    #[test]
1325    fn expression_vectors_build_and_evaluate_scalar_observables() {
1326        let p = Vec4::new(10.0, 3.0, 4.0, 5.0);
1327        assert_eq!(evaluate(p.m2()), Complex64::from(50.0));
1328        assert_eq!(evaluate(p.momentum().mag2()), Complex64::from(50.0));
1329
1330        let a = Vec3::new(1.0, 2.0, 3.0);
1331        let b = Vec3::new(4.0, 5.0, 6.0);
1332        assert_eq!(evaluate(a.dot(&b)), Complex64::from(32.0));
1333        assert_eq!(evaluate(a.cross(&b).z), Complex64::from(-3.0));
1334    }
1335}