Skip to main content

qualia_core_db/solvers/units/
quantity.rs

1//! A dimensioned quantity: a value with a physical [`Dimension`], and arithmetic that
2//! is **dimensionally checked**. Adding incompatible dimensions fails closed; products
3//! and quotients compose dimensions automatically.
4
5use super::dimension::Dimension;
6use super::UnitsError;
7
8/// A value expressed in SI base units, tagged with its physical dimension.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Quantity {
11    /// Magnitude in coherent SI base units (e.g. metres, kilograms, seconds).
12    pub value: f64,
13    pub dimension: Dimension,
14}
15
16impl Quantity {
17    pub const fn new(value: f64, dimension: Dimension) -> Self {
18        Self { value, dimension }
19    }
20
21    /// A dimensionless number.
22    pub const fn scalar(value: f64) -> Self {
23        Self {
24            value,
25            dimension: Dimension::DIMENSIONLESS,
26        }
27    }
28
29    /// Sum of two quantities — requires matching dimensions (fail closed otherwise).
30    pub fn add(&self, other: &Quantity) -> Result<Quantity, UnitsError> {
31        if self.dimension != other.dimension {
32            return Err(UnitsError::IncompatibleDimensions);
33        }
34        Ok(Quantity {
35            value: self.value + other.value,
36            dimension: self.dimension,
37        })
38    }
39
40    /// Difference — requires matching dimensions.
41    pub fn sub(&self, other: &Quantity) -> Result<Quantity, UnitsError> {
42        if self.dimension != other.dimension {
43            return Err(UnitsError::IncompatibleDimensions);
44        }
45        Ok(Quantity {
46            value: self.value - other.value,
47            dimension: self.dimension,
48        })
49    }
50
51    /// Product — values multiply, dimensions compose.
52    pub fn mul(&self, other: &Quantity) -> Quantity {
53        Quantity {
54            value: self.value * other.value,
55            dimension: self.dimension.mul(&other.dimension),
56        }
57    }
58
59    /// Quotient — values divide, dimensions subtract. `None` on divide-by-zero.
60    pub fn div(&self, other: &Quantity) -> Option<Quantity> {
61        if other.value == 0.0 {
62            return None;
63        }
64        Some(Quantity {
65            value: self.value / other.value,
66            dimension: self.dimension.div(&other.dimension),
67        })
68    }
69
70    /// Scale by a dimensionless factor.
71    pub fn scale(&self, factor: f64) -> Quantity {
72        Quantity {
73            value: self.value * factor,
74            dimension: self.dimension,
75        }
76    }
77
78    /// Integer power — value and dimension both raised to `n`.
79    pub fn powi(&self, n: i32) -> Quantity {
80        Quantity {
81            value: self.value.powi(n),
82            dimension: self.dimension.powi(n),
83        }
84    }
85
86    /// `true` iff dimensionally compatible with `other` (can be added/compared).
87    pub fn compatible_with(&self, other: &Quantity) -> bool {
88        self.dimension == other.dimension
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    const EPS: f64 = 1e-9;
96
97    fn metres(v: f64) -> Quantity {
98        Quantity::new(v, Dimension::LENGTH)
99    }
100    fn seconds(v: f64) -> Quantity {
101        Quantity::new(v, Dimension::TIME)
102    }
103
104    #[test]
105    fn adding_like_dimensions_works_unlike_fails() {
106        let total = metres(3.0).add(&metres(4.0)).unwrap();
107        assert!((total.value - 7.0).abs() < EPS);
108        assert_eq!(total.dimension, Dimension::LENGTH);
109        // length + time is a dimensional error.
110        assert_eq!(
111            metres(1.0).add(&seconds(1.0)).unwrap_err(),
112            UnitsError::IncompatibleDimensions
113        );
114    }
115
116    #[test]
117    fn products_derive_new_dimensions() {
118        // distance / time = velocity
119        let v = metres(100.0).div(&seconds(10.0)).unwrap();
120        assert!((v.value - 10.0).abs() < EPS);
121        assert_eq!(v.dimension, Dimension::VELOCITY);
122        // force × distance = energy
123        let force = Quantity::new(5.0, Dimension::FORCE);
124        let work = force.mul(&metres(2.0));
125        assert!((work.value - 10.0).abs() < EPS);
126        assert_eq!(work.dimension, Dimension::ENERGY);
127    }
128
129    #[test]
130    fn kinetic_energy_is_dimensionally_consistent() {
131        // ½ m v²  →  mass × velocity² = energy.
132        let m = Quantity::new(2.0, Dimension::MASS);
133        let v = Quantity::new(3.0, Dimension::VELOCITY);
134        let ke = m.mul(&v.powi(2)).scale(0.5);
135        assert!((ke.value - 9.0).abs() < EPS); // ½·2·9
136        assert_eq!(ke.dimension, Dimension::ENERGY);
137    }
138
139    #[test]
140    fn divide_by_zero_fails_closed() {
141        assert!(metres(1.0).div(&seconds(0.0)).is_none());
142    }
143}