Skip to main content

dualis_units/
vector.rs

1//! Vector quantities: three components sharing one dimension.
2//!
3//! A position, a velocity, a force and a field are all `DVec3` to the compiler,
4//! and adding two of them is a bug the compiler cannot see. [`QVec3`] carries the
5//! same seven exponents as [`Qty`], so a displacement and a velocity stop being
6//! interchangeable.
7//!
8//! Two of the operations are worth noticing, because they fall out of the
9//! dimensions rather than being decided:
10//!
11//! - [`QVec3::normalize`] returns a bare `DVec3`. A direction has no dimension —
12//!   dividing a length by a length leaves a pure number — so a unit vector is
13//!   exactly the right type for "which way", and a ray direction cannot be
14//!   mistaken for a displacement.
15//! - [`QVec3::length`] returns the scalar of the *same* dimension, which needs no
16//!   exponent arithmetic and so works for every dimension at once.
17//!
18//! `dot` and `cross` are missing on purpose: both change the dimension, and there
19//! is no way to express "the square of L" in a const generic parameter without
20//! unstable features. [`QVec3::along`] covers the case that actually comes up —
21//! projecting onto a unit direction, which preserves the dimension.
22
23use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
24
25use glam::DVec3;
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27
28use crate::{Damping, Force, Length, Mass, Qty, Stiffness, Time, Velocity};
29
30/// Three components of one dimension, stored in SI base units.
31#[derive(Clone, Copy, PartialEq, Default)]
32pub struct QVec3<
33    const L: i8,
34    const M: i8,
35    const T: i8,
36    const I: i8,
37    const K: i8,
38    const N: i8,
39    const J: i8,
40>(DVec3);
41
42impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
43    QVec3<L, M, T, I, K, N, J>
44{
45    /// The zero vector, in whatever dimension this is.
46    pub const ZERO: Self = QVec3(DVec3::ZERO);
47
48    /// Wrap a vector already in SI base units.
49    pub fn from_si(v: DVec3) -> Self {
50        QVec3(v)
51    }
52
53    /// The components in SI base units.
54    pub fn to_si(self) -> DVec3 {
55        self.0
56    }
57
58    /// From three quantities of the same dimension.
59    pub fn new(
60        x: Qty<L, M, T, I, K, N, J>,
61        y: Qty<L, M, T, I, K, N, J>,
62        z: Qty<L, M, T, I, K, N, J>,
63    ) -> Self {
64        QVec3(DVec3::new(x.to_si(), y.to_si(), z.to_si()))
65    }
66
67    /// The same quantity in all three components.
68    pub fn splat(v: Qty<L, M, T, I, K, N, J>) -> Self {
69        QVec3(DVec3::splat(v.to_si()))
70    }
71
72    /// The x component, carrying the dimension with it.
73    pub fn x(self) -> Qty<L, M, T, I, K, N, J> {
74        Qty::from_si(self.0.x)
75    }
76
77    /// The y component, carrying the dimension with it.
78    pub fn y(self) -> Qty<L, M, T, I, K, N, J> {
79        Qty::from_si(self.0.y)
80    }
81
82    /// The z component, carrying the dimension with it.
83    pub fn z(self) -> Qty<L, M, T, I, K, N, J> {
84        Qty::from_si(self.0.z)
85    }
86
87    /// Magnitude, which keeps the dimension.
88    pub fn length(self) -> Qty<L, M, T, I, K, N, J> {
89        Qty::from_si(self.0.length())
90    }
91
92    /// Which way it points — a pure number, because a direction is a length over
93    /// a length. Zero-length vectors give zero rather than a NaN.
94    pub fn normalize(self) -> DVec3 {
95        self.0.normalize_or_zero()
96    }
97
98    /// The component along a unit direction. Projection does not change the
99    /// dimension, which is why this one is expressible and `dot` is not.
100    pub fn along(self, direction: DVec3) -> Qty<L, M, T, I, K, N, J> {
101        Qty::from_si(self.0.dot(direction))
102    }
103
104    /// The part of this vector perpendicular to a unit direction.
105    pub fn perpendicular_to(self, direction: DVec3) -> Self {
106        QVec3(self.0 - direction * self.0.dot(direction))
107    }
108
109    /// Whether every component is neither infinite nor NaN.
110    pub fn is_finite(self) -> bool {
111        self.0.is_finite()
112    }
113
114    /// Straight-line interpolation, `t = 0` here and `t = 1` there.
115    ///
116    /// Not clamped, so `t` outside the unit interval extrapolates. Useful for a strobe
117    /// sampling between two recorded states, and wrong for anything that should not leave
118    /// the segment.
119    pub fn lerp(self, other: Self, t: f64) -> Self {
120        QVec3(self.0 + (other.0 - self.0) * t)
121    }
122}
123
124macro_rules! generic_vec_op {
125    ($trait:ident, $method:ident, $op:tt) => {
126        impl<
127                const L: i8,
128                const M: i8,
129                const T: i8,
130                const I: i8,
131                const K: i8,
132                const N: i8,
133                const J: i8,
134            > $trait for QVec3<L, M, T, I, K, N, J>
135        {
136            type Output = Self;
137            fn $method(self, rhs: Self) -> Self {
138                QVec3(self.0 $op rhs.0)
139            }
140        }
141    };
142}
143
144generic_vec_op!(Add, add, +);
145generic_vec_op!(Sub, sub, -);
146
147impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
148    AddAssign for QVec3<L, M, T, I, K, N, J>
149{
150    fn add_assign(&mut self, rhs: Self) {
151        self.0 += rhs.0;
152    }
153}
154
155impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
156    SubAssign for QVec3<L, M, T, I, K, N, J>
157{
158    fn sub_assign(&mut self, rhs: Self) {
159        self.0 -= rhs.0;
160    }
161}
162
163impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8> Neg
164    for QVec3<L, M, T, I, K, N, J>
165{
166    type Output = Self;
167    fn neg(self) -> Self {
168        QVec3(-self.0)
169    }
170}
171
172impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
173    Mul<f64> for QVec3<L, M, T, I, K, N, J>
174{
175    type Output = Self;
176    fn mul(self, k: f64) -> Self {
177        QVec3(self.0 * k)
178    }
179}
180
181impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
182    Div<f64> for QVec3<L, M, T, I, K, N, J>
183{
184    type Output = Self;
185    fn div(self, k: f64) -> Self {
186        QVec3(self.0 / k)
187    }
188}
189
190impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
191    Mul<QVec3<L, M, T, I, K, N, J>> for f64
192{
193    type Output = QVec3<L, M, T, I, K, N, J>;
194    fn mul(self, v: QVec3<L, M, T, I, K, N, J>) -> QVec3<L, M, T, I, K, N, J> {
195        QVec3(v.0 * self)
196    }
197}
198
199impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
200    core::fmt::Debug for QVec3<L, M, T, I, K, N, J>
201{
202    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203        write!(f, "[{}, {}, {}", self.0.x, self.0.y, self.0.z)?;
204        for (symbol, exponent) in [
205            ("m", L),
206            ("kg", M),
207            ("s", T),
208            ("A", I),
209            ("K", K),
210            ("mol", N),
211            ("cd", J),
212        ] {
213            match exponent {
214                0 => {}
215                1 => write!(f, "·{symbol}")?,
216                e => write!(f, "·{symbol}^{e}")?,
217            }
218        }
219        write!(f, "]")
220    }
221}
222
223impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
224    Serialize for QVec3<L, M, T, I, K, N, J>
225{
226    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
227        [self.0.x, self.0.y, self.0.z].serialize(s)
228    }
229}
230
231impl<
232        'de,
233        const L: i8,
234        const M: i8,
235        const T: i8,
236        const I: i8,
237        const K: i8,
238        const N: i8,
239        const J: i8,
240    > Deserialize<'de> for QVec3<L, M, T, I, K, N, J>
241{
242    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
243        <[f64; 3]>::deserialize(d).map(|[x, y, z]| QVec3(DVec3::new(x, y, z)))
244    }
245}
246
247/// A position or a displacement, m. The same dimension, and deliberately the same
248/// type: the difference between them is a choice of origin, not of physics.
249pub type LengthVec = QVec3<1, 0, 0, 0, 0, 0, 0>;
250/// A velocity, m·s⁻¹.
251pub type VelocityVec = QVec3<1, 0, -1, 0, 0, 0, 0>;
252/// An acceleration, m·s⁻².
253pub type AccelerationVec = QVec3<1, 0, -2, 0, 0, 0, 0>;
254/// A force, newtons.
255pub type ForceVec = QVec3<1, 1, -2, 0, 0, 0, 0>;
256/// A momentum, kg·m·s⁻¹. The vector a closed system conserves component by
257/// component — and see the note in the workspace README on why the *smallest* component
258/// is what binds a conservation audit.
259pub type MomentumVec = QVec3<1, 1, -1, 0, 0, 0, 0>;
260
261/// A scalar times a vector, and the division that undoes it.
262macro_rules! scaled_by {
263    ($vec:ty, $scalar:ty => $out:ty) => {
264        impl Mul<$scalar> for $vec {
265            type Output = $out;
266            fn mul(self, k: $scalar) -> $out {
267                QVec3(self.0 * k.to_si())
268            }
269        }
270        impl Mul<$vec> for $scalar {
271            type Output = $out;
272            fn mul(self, v: $vec) -> $out {
273                QVec3(v.0 * self.to_si())
274            }
275        }
276        impl Div<$scalar> for $out {
277            type Output = $vec;
278            fn div(self, k: $scalar) -> $vec {
279                QVec3(self.0 / k.to_si())
280            }
281        }
282    };
283}
284
285scaled_by!(VelocityVec, Time => LengthVec);
286scaled_by!(AccelerationVec, Time => VelocityVec);
287scaled_by!(ForceVec, Time => MomentumVec);
288scaled_by!(VelocityVec, Mass => MomentumVec);
289scaled_by!(AccelerationVec, Mass => ForceVec);
290// Hooke's law and a dashpot, as vectors: the two forces a penalty contact is made
291// of, and the two that make its stability limit what it is.
292scaled_by!(LengthVec, Stiffness => ForceVec);
293scaled_by!(VelocityVec, Damping => ForceVec);
294
295impl LengthVec {
296    /// Millimetres.
297    pub fn mm(x: f64, y: f64, z: f64) -> LengthVec {
298        QVec3(DVec3::new(x, y, z) * 1e-3)
299    }
300    /// Metres.
301    pub fn m(x: f64, y: f64, z: f64) -> LengthVec {
302        QVec3(DVec3::new(x, y, z))
303    }
304    /// As millimetres.
305    pub fn in_mm(self) -> DVec3 {
306        self.0 * 1e3
307    }
308}
309
310impl VelocityVec {
311    /// Millimetres per second.
312    pub fn mm_per_s(x: f64, y: f64, z: f64) -> VelocityVec {
313        QVec3(DVec3::new(x, y, z) * 1e-3)
314    }
315    /// Metres per second.
316    pub fn m_per_s(x: f64, y: f64, z: f64) -> VelocityVec {
317        QVec3(DVec3::new(x, y, z))
318    }
319}
320
321/// Distance between two points, which is what a length actually measures.
322pub fn distance(a: LengthVec, b: LengthVec) -> Length {
323    (a - b).length()
324}
325
326/// Newton's second law, with the dimensions doing the checking.
327pub fn newton_second(mass: Mass, acceleration: AccelerationVec) -> ForceVec {
328    mass * acceleration
329}
330
331/// Momentum of a moving mass.
332pub fn momentum(mass: Mass, velocity: VelocityVec) -> MomentumVec {
333    mass * velocity
334}
335
336/// Kinetic energy, ½mv². Needs the squared magnitude, so it is written out here
337/// rather than falling out of an operator.
338pub fn kinetic_energy(mass: Mass, velocity: VelocityVec) -> crate::Energy {
339    let v = velocity.to_si().length();
340    Qty::from_si(0.5 * mass.to_si() * v * v)
341}
342
343/// Speed acquired, and distance covered, under a constant acceleration.
344pub fn free_travel(v0: VelocityVec, a: AccelerationVec, t: Time) -> (VelocityVec, LengthVec) {
345    let v = v0 + a * t;
346    let x = v0 * t + (a * t) * t * 0.5;
347    (v, x)
348}
349
350/// Force needed to hold `mass` in a circle — the check that a rotating stage's
351/// bearing can take what a scan rate asks of it.
352pub fn centripetal(mass: Mass, speed: Velocity, radius: Length) -> Force {
353    Qty::from_si(mass.to_si() * speed.to_si() * speed.to_si() / radius.to_si())
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::{Energy, Temperature};
360
361    /// A direction is dimensionless, which means a ray direction and a
362    /// displacement are different types and cannot be swapped.
363    #[test]
364    fn normalising_strips_the_dimension() {
365        let d = LengthVec::mm(3.0, 4.0, 0.0);
366        assert!((d.length().in_mm() - 5.0).abs() < 1e-12);
367        let dir: DVec3 = d.normalize();
368        assert!((dir.length() - 1.0).abs() < 1e-15);
369        assert!((dir - DVec3::new(0.6, 0.8, 0.0)).length() < 1e-15);
370        // A zero vector has no direction, and says so rather than producing NaN.
371        assert_eq!(LengthVec::ZERO.normalize(), DVec3::ZERO);
372    }
373
374    /// Projection preserves the dimension, which is what makes it expressible
375    /// where `dot` is not.
376    #[test]
377    fn projection_keeps_the_dimension() {
378        let v = VelocityVec::mm_per_s(120.0, 0.0, -5.0);
379        let axis = DVec3::X;
380        let along: Velocity = v.along(axis);
381        assert!((along.to_si() - 0.12).abs() < 1e-15);
382        let across = v.perpendicular_to(axis);
383        assert!((across.along(axis).to_si()).abs() < 1e-15);
384        // The two parts add back up to the whole.
385        let rebuilt = across + VelocityVec::from_si(axis * along.to_si());
386        assert!((rebuilt - v).length().to_si() < 1e-15);
387    }
388
389    /// Kinematics with the dimensions doing the bookkeeping: 1 g for 2 s gives
390    /// 19.6 m/s and 19.6 m, which are different numbers of different dimensions
391    /// that happen to share digits.
392    #[test]
393    fn constant_acceleration_is_dimensionally_checked() {
394        let a = AccelerationVec::from_si(DVec3::new(0.0, -crate::G0.to_si(), 0.0));
395        let (v, x) = free_travel(VelocityVec::ZERO, a, Time::s(2.0));
396        assert!((v.length().to_si() - 19.6133).abs() < 1e-3, "{v:?}");
397        assert!((x.length().to_si() - 19.6133).abs() < 1e-3, "{x:?}");
398        // Downwards, both of them.
399        assert!(v.y().to_si() < 0.0 && x.y().to_si() < 0.0);
400    }
401
402    /// Newton's second law and the energy it does, checked against the closed
403    /// form: work done equals the kinetic energy gained.
404    #[test]
405    fn work_equals_the_kinetic_energy_it_bought() {
406        let m = Mass::kg(2.0);
407        let a = AccelerationVec::from_si(DVec3::X * 3.0);
408        let f: ForceVec = newton_second(m, a);
409        assert!((f.length().to_si() - 6.0).abs() < 1e-12);
410
411        let t = Time::s(4.0);
412        let (v, x) = free_travel(VelocityVec::ZERO, a, t);
413        let work: Energy = Qty::from_si(f.along(DVec3::X).to_si() * x.along(DVec3::X).to_si());
414        let ke = kinetic_energy(m, v);
415        assert!(
416            (work - ke).abs().to_si() < 1e-9,
417            "work {work:?} should equal kinetic energy {ke:?}"
418        );
419    }
420
421    /// Momentum is conserved in a collision, and the type system will not let a
422    /// velocity be added to a momentum on the way there.
423    #[test]
424    fn momentum_adds_across_a_collision() {
425        let p1 = momentum(Mass::kg(2.0), VelocityVec::m_per_s(3.0, 0.0, 0.0));
426        let p2 = momentum(Mass::kg(1.0), VelocityVec::m_per_s(-4.0, 0.0, 0.0));
427        let total: MomentumVec = p1 + p2;
428        assert!((total.along(DVec3::X).to_si() - 2.0).abs() < 1e-12);
429        // The combined mass therefore moves at 2/3 m/s.
430        let after: VelocityVec = total / Mass::kg(3.0);
431        assert!((after.along(DVec3::X).to_si() - 2.0 / 3.0).abs() < 1e-12);
432    }
433
434    #[test]
435    fn vectors_round_trip_through_json() {
436        let v = LengthVec::mm(1.0, 2.0, 3.0);
437        let json = serde_json::to_string(&v).unwrap();
438        assert_eq!(json, "[0.001,0.002,0.003]");
439        assert_eq!(serde_json::from_str::<LengthVec>(&json).unwrap(), v);
440    }
441
442    #[test]
443    fn debug_shows_the_dimension() {
444        assert_eq!(
445            format!("{:?}", ForceVec::from_si(DVec3::new(1.0, 0.0, 0.0))),
446            "[1, 0, 0·m·kg·s^-2]"
447        );
448    }
449
450    /// The whole point, stated as a compile-time fact rather than a runtime one:
451    /// these lines do not compile, and the comments are the test.
452    #[test]
453    fn wrong_dimensions_do_not_compile() {
454        let _ = LengthVec::mm(1.0, 0.0, 0.0);
455        let _ = VelocityVec::mm_per_s(1.0, 0.0, 0.0);
456        let _ = Temperature::kelvin(300.0);
457        // let _ = _position + _velocity;        // mismatched types
458        // let _ = _position.along(_velocity);   // expected DVec3
459        // let _: Length = _temperature;         // mismatched types
460    }
461}