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::{CompileOptions, 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        // The fixtures check vector algebra against direct numerical formulae,
998        // independent of the compiler's separately tested optimization passes.
999        let model =
1000            CompiledModel::from_expr_with_options(&expr, &CompileOptions::without_optimizations())
1001                .unwrap();
1002        let params = model.params().default_values();
1003        CpuBackend.prepare(&model).evaluate(&params).unwrap()
1004    }
1005
1006    fn evaluate_real(expr: Expr) -> f64 {
1007        evaluate(expr).re
1008    }
1009
1010    fn assert_symbolic_vec4_eq(symbolic: Vec4, real: RealVec4, epsilon: f64) {
1011        assert_relative_eq!(evaluate_real(symbolic.e), real.e, epsilon = epsilon);
1012        assert_relative_eq!(evaluate_real(symbolic.px), real.px, epsilon = epsilon);
1013        assert_relative_eq!(evaluate_real(symbolic.py), real.py, epsilon = epsilon);
1014        assert_relative_eq!(evaluate_real(symbolic.pz), real.pz, epsilon = epsilon);
1015    }
1016
1017    #[test]
1018    fn test_display() {
1019        let v3 = RealVec3::new(1.2341, -2.3452, 3.4563);
1020        assert_eq!(format!("{}", v3), "[ 1.234, -2.345,  3.456]");
1021        let v4 = RealVec4::new(4.5674, 1.2341, -2.3452, 3.4563);
1022        assert_eq!(format!("{}", v4), "[ 4.567;  1.234, -2.345,  3.456]");
1023    }
1024
1025    #[test]
1026    fn test_vec_vector_conversion() {
1027        let v = RealVec3::new(1.0, 2.0, 3.0);
1028        let vector3: Vec<f64> = v.into();
1029        assert_eq!(vector3[0], 1.0);
1030        assert_eq!(vector3[1], 2.0);
1031        assert_eq!(vector3[2], 3.0);
1032
1033        let v_from_vec: RealVec3 = vector3.try_into().unwrap();
1034        assert_eq!(v_from_vec, v);
1035
1036        let v = RealVec4::new(1.0, 2.0, 3.0, 4.0);
1037        let vector4: Vec<f64> = v.into();
1038        assert_eq!(vector4[0], 1.0);
1039        assert_eq!(vector4[1], 2.0);
1040        assert_eq!(vector4[2], 3.0);
1041        assert_eq!(vector4[3], 4.0);
1042
1043        let v_from_vec: RealVec4 = vector4.try_into().unwrap();
1044        assert_eq!(v_from_vec, v);
1045    }
1046
1047    #[test]
1048    fn test_vec_array_conversion() {
1049        let arr = [1.0, 2.0, 3.0];
1050        let v: RealVec3 = arr.into();
1051        assert_eq!(v, RealVec3::new(1.0, 2.0, 3.0));
1052
1053        let back_to_array: [f64; 3] = v.into();
1054        assert_eq!(back_to_array, arr);
1055
1056        let arr = [1.0, 2.0, 3.0, 4.0];
1057        let v: RealVec4 = arr.into();
1058        assert_eq!(v, RealVec4::new(1.0, 2.0, 3.0, 4.0));
1059
1060        let back_to_array: [f64; 4] = v.into();
1061        assert_eq!(back_to_array, arr);
1062        assert_eq!(v.components(), arr);
1063    }
1064
1065    #[test]
1066    fn test_vec_nalgebra_conversion() {
1067        let v = RealVec3::new(1.0, 2.0, 3.0);
1068        let vector3: Vector3<f64> = v.into();
1069        assert_eq!(vector3.x, 1.0);
1070        assert_eq!(vector3.y, 2.0);
1071        assert_eq!(vector3.z, 3.0);
1072
1073        let v_from_vec: RealVec3 = vector3.into();
1074        assert_eq!(v_from_vec, v);
1075
1076        let v = RealVec4::new(1.0, 2.0, 3.0, 4.0);
1077        let vector4: Vector4<f64> = v.into();
1078        assert_eq!(vector4.x, 1.0);
1079        assert_eq!(vector4.y, 2.0);
1080        assert_eq!(vector4.z, 3.0);
1081        assert_eq!(vector4.w, 4.0);
1082
1083        let v_from_vec: RealVec4 = vector4.into();
1084        assert_eq!(v_from_vec, v);
1085    }
1086
1087    #[test]
1088    fn test_vec_sums() {
1089        let vectors = [RealVec3::new(1.0, 2.0, 3.0), RealVec3::new(4.0, 5.0, 6.0)];
1090        let sum: RealVec3 = vectors.iter().sum();
1091        assert_eq!(sum, RealVec3::new(5.0, 7.0, 9.0));
1092        let sum: RealVec3 = vectors.into_iter().sum();
1093        assert_eq!(sum, RealVec3::new(5.0, 7.0, 9.0));
1094
1095        let vectors = [
1096            RealVec4::new(1.0, 2.0, 3.0, 4.0),
1097            RealVec4::new(4.0, 5.0, 6.0, 7.0),
1098        ];
1099        let sum: RealVec4 = vectors.iter().sum();
1100        assert_eq!(sum, RealVec4::new(5.0, 7.0, 9.0, 11.0));
1101        let sum: RealVec4 = vectors.into_iter().sum();
1102        assert_eq!(sum, RealVec4::new(5.0, 7.0, 9.0, 11.0));
1103    }
1104
1105    #[test]
1106    fn test_three_to_four_momentum_conversion() {
1107        let p3 = RealVec3::new(1.0, 2.0, 3.0);
1108        let target_p4 = RealVec4::new(10.0, 1.0, 2.0, 3.0);
1109        let p4_from_mass = p3.with_mass(target_p4.m().unwrap());
1110        assert_eq!(target_p4.e(), p4_from_mass.e());
1111        assert_eq!(target_p4.px(), p4_from_mass.px());
1112        assert_eq!(target_p4.py(), p4_from_mass.py());
1113        assert_eq!(target_p4.pz(), p4_from_mass.pz());
1114        let p4_from_energy = p3.with_energy(target_p4.e());
1115        assert_eq!(target_p4.e(), p4_from_energy.e());
1116        assert_eq!(target_p4.px(), p4_from_energy.px());
1117        assert_eq!(target_p4.py(), p4_from_energy.py());
1118        assert_eq!(target_p4.pz(), p4_from_energy.pz());
1119    }
1120
1121    #[test]
1122    fn test_four_momentum_basics() {
1123        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1124        assert_eq!(p.e(), 10.0);
1125        assert_eq!(p.px(), 3.0);
1126        assert_eq!(p.py(), 4.0);
1127        assert_eq!(p.pz(), 5.0);
1128        assert_eq!(p.momentum().px(), 3.0);
1129        assert_eq!(p.momentum().py(), 4.0);
1130        assert_eq!(p.momentum().pz(), 5.0);
1131        assert_relative_eq!(p.beta().unwrap().x, 0.3);
1132        assert_relative_eq!(p.beta().unwrap().y, 0.4);
1133        assert_relative_eq!(p.beta().unwrap().z, 0.5);
1134        assert_relative_eq!(p.m2(), 50.0);
1135        assert_relative_eq!(p.m().unwrap(), f64::sqrt(50.0));
1136        assert_eq!(
1137            p.to_p4_string().to_string(),
1138            "[e = 10.00000; p = (3.00000, 4.00000, 5.00000); m = 7.07107]"
1139        );
1140        assert_relative_eq!(RealVec3::x().x, 1.0);
1141        assert_relative_eq!(RealVec3::x().y, 0.0);
1142        assert_relative_eq!(RealVec3::x().z, 0.0);
1143        assert_relative_eq!(RealVec3::y().x, 0.0);
1144        assert_relative_eq!(RealVec3::y().y, 1.0);
1145        assert_relative_eq!(RealVec3::y().z, 0.0);
1146        assert_relative_eq!(RealVec3::z().x, 0.0);
1147        assert_relative_eq!(RealVec3::z().y, 0.0);
1148        assert_relative_eq!(RealVec3::z().z, 1.0);
1149        assert_relative_eq!(RealVec3::default().x, 0.0);
1150        assert_relative_eq!(RealVec3::default().y, 0.0);
1151        assert_relative_eq!(RealVec3::default().z, 0.0);
1152    }
1153
1154    #[test]
1155    fn test_three_momentum_basics() {
1156        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1157        let q = RealVec4::new(0.0, 1.2, -3.4, 7.6);
1158        let p3_view = p.momentum();
1159        let q3_view = q.momentum();
1160        assert_eq!(p3_view.px(), 3.0);
1161        assert_eq!(p3_view.py(), 4.0);
1162        assert_eq!(p3_view.pz(), 5.0);
1163        assert_relative_eq!(p3_view.mag2(), 50.0);
1164        assert_relative_eq!(p3_view.mag(), f64::sqrt(50.0));
1165        assert_relative_eq!(p3_view.costheta().unwrap(), 5.0 / f64::sqrt(50.0));
1166        assert_relative_eq!(p3_view.theta().unwrap(), f64::acos(5.0 / f64::sqrt(50.0)));
1167        assert_relative_eq!(p3_view.phi(), f64::atan2(4.0, 3.0));
1168        assert_relative_eq!(
1169            p3_view.unit().unwrap(),
1170            RealVec3::new(
1171                3.0 / f64::sqrt(50.0),
1172                4.0 / f64::sqrt(50.0),
1173                5.0 / f64::sqrt(50.0)
1174            )
1175        );
1176        assert_relative_eq!(p3_view.cross(&q3_view), RealVec3::new(47.4, -16.8, -15.0));
1177    }
1178
1179    #[test]
1180    fn test_vec_equality() {
1181        let p = RealVec3::new(1.1, 2.2, 3.3);
1182        let p2 = RealVec3::new(1.1 * 2.0, 2.2 * 2.0, 3.3 * 2.0);
1183        assert_abs_diff_eq!(p * 2.0, p2);
1184        assert_relative_eq!(p * 2.0, p2);
1185    }
1186
1187    #[test]
1188    fn test_boost_com() {
1189        let p = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1190        let zero = p.boost(&-p.beta().unwrap()).momentum();
1191        assert_relative_eq!(zero, RealVec3::zero());
1192    }
1193
1194    #[test]
1195    fn test_boost() {
1196        let p0 = RealVec4::new(1.0, 0.0, 0.0, 0.0);
1197        assert_relative_eq!(p0.gamma().unwrap(), 1.0);
1198        let p0 = RealVec4::new(1.0, f64::sqrt(3.0) / 2.0, 0.0, 0.0);
1199        assert_relative_eq!(p0.gamma().unwrap(), 2.0);
1200        let p1 = RealVec4::new(10.0, 3.0, 4.0, 5.0);
1201        let p2 = RealVec4::new(9.0, 3.4, 2.3, 1.2);
1202        let p1_boosted = p1.boost(&-p2.beta().unwrap());
1203        assert_relative_eq!(p1_boosted.e(), 8.157632144622882);
1204        assert_relative_eq!(p1_boosted.px(), -0.6489200627053444);
1205        assert_relative_eq!(p1_boosted.py(), 1.5316128987581492);
1206        assert_relative_eq!(p1_boosted.pz(), 3.712145860221643);
1207    }
1208
1209    #[test]
1210    fn real_and_symbolic_vector_formulas_agree_on_finite_inputs() {
1211        let mut rng = Rng::with_seed(0x5645_4354_4f52);
1212
1213        for _ in 0..32 {
1214            let a3 = RealVec3::new(
1215                rng.f64() * 10.0 - 5.0,
1216                rng.f64() * 10.0 - 5.0,
1217                rng.f64() * 10.0 - 5.0,
1218            );
1219            let b3 = RealVec3::new(
1220                rng.f64() * 10.0 - 5.0,
1221                rng.f64() * 10.0 - 5.0,
1222                rng.f64() * 10.0 - 5.0,
1223            );
1224            let a = Vec3::from(a3);
1225            let b = Vec3::from(b3);
1226            assert_relative_eq!(evaluate_real(a.dot(&b)), a3.dot(&b3), epsilon = 1e-11);
1227            let symbolic_cross = a.cross(&b);
1228            let real_cross = a3.cross(&b3);
1229            assert_relative_eq!(
1230                evaluate_real(symbolic_cross.x),
1231                real_cross.x,
1232                epsilon = 1e-11
1233            );
1234            assert_relative_eq!(
1235                evaluate_real(symbolic_cross.y),
1236                real_cross.y,
1237                epsilon = 1e-11
1238            );
1239            assert_relative_eq!(
1240                evaluate_real(symbolic_cross.z),
1241                real_cross.z,
1242                epsilon = 1e-11
1243            );
1244
1245            let p = RealVec4::new(
1246                rng.f64() * 9.0 + 1.0,
1247                rng.f64() * 10.0 - 5.0,
1248                rng.f64() * 10.0 - 5.0,
1249                rng.f64() * 10.0 - 5.0,
1250            );
1251            let q = RealVec4::new(
1252                rng.f64() * 9.0 + 1.0,
1253                rng.f64() * 10.0 - 5.0,
1254                rng.f64() * 10.0 - 5.0,
1255                rng.f64() * 10.0 - 5.0,
1256            );
1257            let symbolic_p = Vec4::from(p);
1258            let symbolic_q = Vec4::from(q);
1259            assert_relative_eq!(
1260                evaluate_real(symbolic_p.dot(&symbolic_q)),
1261                p.dot(&q),
1262                epsilon = 1e-11
1263            );
1264            assert_relative_eq!(evaluate_real(symbolic_p.m2()), p.m2(), epsilon = 1e-11);
1265
1266            let beta = RealVec3::new(
1267                rng.f64() * 0.6 - 0.3,
1268                rng.f64() * 0.6 - 0.3,
1269                rng.f64() * 0.6 - 0.3,
1270            );
1271            assert_symbolic_vec4_eq(
1272                Vec4::from(p).boost(&Vec3::from(beta)),
1273                p.boost(&beta),
1274                1e-10,
1275            );
1276        }
1277    }
1278
1279    #[test]
1280    fn vector_formula_boundaries_remain_well_defined() {
1281        let p = RealVec4::new(2.0, 0.25, -0.5, 1.0);
1282        assert_eq!(p.boost(&RealVec3::zero()), p);
1283        assert_symbolic_vec4_eq(Vec4::from(p).boost(&Vec3::zero()), p, 1e-12);
1284
1285        let near_zero = RealVec3::new(1e-12, -1e-12, 1e-12);
1286        assert_symbolic_vec4_eq(
1287            Vec4::from(p).boost(&Vec3::from(near_zero)),
1288            p.boost(&near_zero),
1289            1e-12,
1290        );
1291
1292        let near_lightlike = RealVec4::new(1.0, 1.0 - 1e-12, 0.0, 0.0);
1293        assert_relative_eq!(
1294            evaluate_real(Vec4::from(near_lightlike).m2()),
1295            near_lightlike.m2(),
1296            epsilon = 1e-15
1297        );
1298
1299        let near_light_speed = RealVec3::new(1.0 - 1e-12, 0.0, 0.0);
1300        assert_symbolic_vec4_eq(
1301            Vec4::from(p).boost(&Vec3::from(near_light_speed)),
1302            p.boost(&near_light_speed),
1303            1e-6,
1304        );
1305    }
1306
1307    #[test]
1308    fn collinear_frame_composition_matches_combined_boost() {
1309        let p = RealVec4::new(5.0, 0.4, -0.2, 1.5);
1310        let first = 0.2;
1311        let second = -0.35;
1312        let combined = (first + second) / (1.0 + first * second);
1313
1314        let sequential = p
1315            .boost(&RealVec3::new(first, 0.0, 0.0))
1316            .boost(&RealVec3::new(second, 0.0, 0.0));
1317        let direct = p.boost(&RealVec3::new(combined, 0.0, 0.0));
1318        assert_relative_eq!(sequential, direct, epsilon = 1e-12);
1319        assert_symbolic_vec4_eq(
1320            Vec4::from(p)
1321                .boost(&Vec3::new(first, 0.0, 0.0))
1322                .boost(&Vec3::new(second, 0.0, 0.0)),
1323            sequential,
1324            1e-12,
1325        );
1326    }
1327
1328    #[test]
1329    fn expression_vectors_build_and_evaluate_scalar_observables() {
1330        let p = Vec4::new(10.0, 3.0, 4.0, 5.0);
1331        assert_eq!(evaluate(p.m2()), Complex64::from(50.0));
1332        assert_eq!(evaluate(p.momentum().mag2()), Complex64::from(50.0));
1333
1334        let a = Vec3::new(1.0, 2.0, 3.0);
1335        let b = Vec3::new(4.0, 5.0, 6.0);
1336        assert_eq!(evaluate(a.dot(&b)), Complex64::from(32.0));
1337        assert_eq!(evaluate(a.cross(&b).z), Complex64::from(-3.0));
1338    }
1339}