Skip to main content

logicaffeine_base/
dimension.rs

1//! Physical dimensions as an abelian group of rational exponent vectors.
2//!
3//! A *dimension* is what a quantity measures — length, mass, time, or a product/quotient of
4//! them (area = L², speed = L·T⁻¹, force = M·L·T⁻²). It is NOT a unit: metres and feet share
5//! the dimension Length. We model a dimension as a vector of rational exponents over the base
6//! dimensions, so dimensional algebra is exact vector arithmetic:
7//!
8//! - `×` adds exponent vectors (`Length × Length = Length²`),
9//! - `÷` subtracts them (`Area ÷ Length = Length`),
10//! - integer powers scale them, and the `n`th root divides them (so `√(Length²) = Length`).
11//!
12//! Exponents are *rational* (not just integer) because real derived quantities need fractional
13//! powers — noise density is `V·Hz^(−1/2)`. The group's identity is the dimensionless vector;
14//! every dimension has an inverse (`recip`). `Dimension` is `Copy + Eq + Hash`, so it is a cheap
15//! catalog key, rides inside the compiler's type lattice, and tags a runtime quantity.
16
17use std::fmt;
18
19/// The base dimensions: the SI seven, plus the extensions Logos tracks end-to-end. Plane angle
20/// and solid angle are SI-dimensionless but tracked here to catch radian/steradian mix-ups;
21/// information (the bit) is tracked so data sizes are first-class. "Dimensionless" is the
22/// all-zero vector, not an axis.
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
24#[repr(usize)]
25pub enum BaseDim {
26    Length = 0,
27    Mass,
28    Time,
29    Current,
30    Temperature,
31    Amount,
32    Luminous,
33    Angle,
34    SolidAngle,
35    Information,
36}
37
38impl BaseDim {
39    /// The number of base axes (the length of a [`Dimension`]'s exponent vector).
40    pub const COUNT: usize = 10;
41
42    /// Every base dimension, in canonical order (the order exponents are stored and displayed).
43    pub const ALL: [BaseDim; BaseDim::COUNT] = [
44        BaseDim::Length,
45        BaseDim::Mass,
46        BaseDim::Time,
47        BaseDim::Current,
48        BaseDim::Temperature,
49        BaseDim::Amount,
50        BaseDim::Luminous,
51        BaseDim::Angle,
52        BaseDim::SolidAngle,
53        BaseDim::Information,
54    ];
55
56    /// This axis's index into the exponent vector.
57    #[inline]
58    pub const fn index(self) -> usize {
59        self as usize
60    }
61
62    /// The conventional one-symbol tag used when displaying a dimension signature.
63    pub const fn symbol(self) -> &'static str {
64        match self {
65            BaseDim::Length => "L",
66            BaseDim::Mass => "M",
67            BaseDim::Time => "T",
68            BaseDim::Current => "I",
69            BaseDim::Temperature => "Θ",
70            BaseDim::Amount => "N",
71            BaseDim::Luminous => "J",
72            BaseDim::Angle => "rad",
73            BaseDim::SolidAngle => "sr",
74            BaseDim::Information => "bit",
75        }
76    }
77}
78
79/// `gcd(|a|, |b|)` by the Euclidean algorithm (`gcd(a, 0) == |a|`).
80const fn igcd(mut a: i32, mut b: i32) -> i32 {
81    a = a.abs();
82    b = b.abs();
83    while b != 0 {
84        let t = b;
85        b = a % b;
86        a = t;
87    }
88    a
89}
90
91/// A signed rational exponent in lowest terms with a strictly positive denominator — so equal
92/// exponents share one representation (`Eq`/`Hash` are structural). Admits fractional powers
93/// (`L^(1/2)`) while the common integer case is just `den == 1`.
94#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
95pub struct Exp {
96    num: i32,
97    den: i32,
98}
99
100impl Exp {
101    /// The zero exponent (an axis that does not appear).
102    pub const ZERO: Exp = Exp { num: 0, den: 1 };
103    /// The unit exponent.
104    pub const ONE: Exp = Exp { num: 1, den: 1 };
105
106    /// Reduce `num/den` to lowest terms with a positive denominator. Panics on a zero denominator.
107    pub fn new(num: i32, den: i32) -> Exp {
108        assert!(den != 0, "exponent denominator must be nonzero");
109        let (mut num, mut den) = (num, den);
110        if den < 0 {
111            num = -num;
112            den = -den;
113        }
114        if num == 0 {
115            return Exp::ZERO;
116        }
117        let g = igcd(num, den);
118        Exp { num: num / g, den: den / g }
119    }
120
121    /// The whole exponent `n` (`n/1`).
122    #[inline]
123    pub const fn int(n: i32) -> Exp {
124        Exp { num: n, den: 1 }
125    }
126
127    pub const fn numerator(self) -> i32 {
128        self.num
129    }
130
131    pub const fn denominator(self) -> i32 {
132        self.den
133    }
134
135    #[inline]
136    pub const fn is_zero(self) -> bool {
137        self.num == 0
138    }
139
140    /// `self + other` (exact rational addition).
141    pub fn add(self, other: Exp) -> Exp {
142        Exp::new(self.num * other.den + other.num * self.den, self.den * other.den)
143    }
144
145    /// `self − other`.
146    pub fn sub(self, other: Exp) -> Exp {
147        Exp::new(self.num * other.den - other.num * self.den, self.den * other.den)
148    }
149
150    /// `−self`.
151    pub fn neg(self) -> Exp {
152        Exp { num: -self.num, den: self.den }
153    }
154
155    /// `self · k` — scaling by an integer (raising a dimension to the `k`th power).
156    pub fn scale(self, k: i32) -> Exp {
157        Exp::new(self.num * k, self.den)
158    }
159
160    /// `self / k` — dividing the exponent by an integer (taking the `k`th root). Panics for `k == 0`.
161    pub fn div_int(self, k: i32) -> Exp {
162        Exp::new(self.num, self.den * k)
163    }
164}
165
166impl fmt::Display for Exp {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        if self.den == 1 {
169            write!(f, "{}", self.num)
170        } else {
171            write!(f, "{}/{}", self.num, self.den)
172        }
173    }
174}
175
176/// A physical dimension: a vector of rational exponents over [`BaseDim`]. The group operation is
177/// `mul` (axiswise exponent addition); the identity is [`Dimension::DIMENSIONLESS`]; the inverse
178/// is [`Dimension::recip`].
179#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
180pub struct Dimension {
181    e: [Exp; BaseDim::COUNT],
182}
183
184impl Dimension {
185    /// The identity dimension — the all-zero exponent vector (a pure number, e.g. a count or ratio).
186    pub const DIMENSIONLESS: Dimension = Dimension { e: [Exp::ZERO; BaseDim::COUNT] };
187
188    /// The dimension of a single base axis raised to the first power (e.g. `base(Length)` = L).
189    pub fn base(d: BaseDim) -> Dimension {
190        let mut e = [Exp::ZERO; BaseDim::COUNT];
191        e[d.index()] = Exp::ONE;
192        Dimension { e }
193    }
194
195    /// Build a dimension directly from its exponent vector (in [`BaseDim::ALL`] order) — the inverse
196    /// of reading [`exponent`](Self::exponent) for each axis. Used to reconstruct a dimension from
197    /// the wire, where the exponents travel as their `(numerator, denominator)` pairs.
198    pub fn from_exps(e: [Exp; BaseDim::COUNT]) -> Dimension {
199        Dimension { e }
200    }
201
202    /// The exponent on a given base axis.
203    #[inline]
204    pub fn exponent(self, d: BaseDim) -> Exp {
205        self.e[d.index()]
206    }
207
208    /// True for the dimensionless identity (every exponent zero).
209    pub fn is_dimensionless(self) -> bool {
210        self.e.iter().all(|x| x.is_zero())
211    }
212
213    /// `self × other` — axiswise exponent ADDITION (`Length × Length = Length²`).
214    pub fn mul(self, other: Dimension) -> Dimension {
215        let mut e = self.e;
216        for (i, slot) in e.iter_mut().enumerate() {
217            *slot = slot.add(other.e[i]);
218        }
219        Dimension { e }
220    }
221
222    /// `self ÷ other` — axiswise exponent SUBTRACTION (`Area ÷ Length = Length`).
223    pub fn div(self, other: Dimension) -> Dimension {
224        let mut e = self.e;
225        for (i, slot) in e.iter_mut().enumerate() {
226            *slot = slot.sub(other.e[i]);
227        }
228        Dimension { e }
229    }
230
231    /// `1 / self` — the multiplicative inverse (negate every exponent). `Frequency = recip(Time)`.
232    pub fn recip(self) -> Dimension {
233        let mut e = self.e;
234        for slot in e.iter_mut() {
235            *slot = slot.neg();
236        }
237        Dimension { e }
238    }
239
240    /// `self^k` — the `k`th power (scale every exponent). `powi(0)` is dimensionless.
241    pub fn powi(self, k: i32) -> Dimension {
242        let mut e = self.e;
243        for slot in e.iter_mut() {
244            *slot = slot.scale(k);
245        }
246        Dimension { e }
247    }
248
249    /// The `k`th root — divide every exponent by `k` (`nth_root(Area, 2) = Length`). Panics for `k == 0`.
250    pub fn nth_root(self, k: i32) -> Dimension {
251        let mut e = self.e;
252        for slot in e.iter_mut() {
253            *slot = slot.div_int(k);
254        }
255        Dimension { e }
256    }
257
258    // ---- The SI base dimensions, by name ----
259    pub fn length() -> Dimension { Dimension::base(BaseDim::Length) }
260    pub fn mass() -> Dimension { Dimension::base(BaseDim::Mass) }
261    pub fn time() -> Dimension { Dimension::base(BaseDim::Time) }
262    pub fn current() -> Dimension { Dimension::base(BaseDim::Current) }
263    pub fn temperature() -> Dimension { Dimension::base(BaseDim::Temperature) }
264    pub fn amount() -> Dimension { Dimension::base(BaseDim::Amount) }
265    pub fn luminous() -> Dimension { Dimension::base(BaseDim::Luminous) }
266    pub fn angle() -> Dimension { Dimension::base(BaseDim::Angle) }
267    pub fn solid_angle() -> Dimension { Dimension::base(BaseDim::SolidAngle) }
268    pub fn information() -> Dimension { Dimension::base(BaseDim::Information) }
269
270    // ---- Common derived dimensions, composed from the base ones ----
271    pub fn area() -> Dimension { Self::length().powi(2) }
272    pub fn volume() -> Dimension { Self::length().powi(3) }
273    pub fn speed() -> Dimension { Self::length().div(Self::time()) }
274    pub fn acceleration() -> Dimension { Self::speed().div(Self::time()) }
275    pub fn frequency() -> Dimension { Self::time().recip() }
276    pub fn force() -> Dimension { Self::mass().mul(Self::acceleration()) }
277    pub fn energy() -> Dimension { Self::force().mul(Self::length()) }
278    pub fn power() -> Dimension { Self::energy().div(Self::time()) }
279    pub fn pressure() -> Dimension { Self::force().div(Self::area()) }
280    pub fn charge() -> Dimension { Self::current().mul(Self::time()) }
281    pub fn voltage() -> Dimension { Self::power().div(Self::current()) }
282    pub fn resistance() -> Dimension { Self::voltage().div(Self::current()) }
283    pub fn conductance() -> Dimension { Self::resistance().recip() } // siemens: A/V = Ω⁻¹
284    pub fn capacitance() -> Dimension { Self::charge().div(Self::voltage()) } // farad: C/V
285    pub fn magnetic_flux() -> Dimension { Self::voltage().mul(Self::time()) } // weber: V·s
286    pub fn inductance() -> Dimension { Self::magnetic_flux().div(Self::current()) } // henry: Wb/A
287    pub fn magnetic_flux_density() -> Dimension { Self::magnetic_flux().div(Self::area()) } // tesla: Wb/m²
288    pub fn surface_tension() -> Dimension { Self::force().div(Self::length()) } // N/m = M·T⁻²
289    pub fn density() -> Dimension { Self::mass().div(Self::volume()) }
290    // Radiation.
291    pub fn absorbed_dose() -> Dimension { Self::energy().div(Self::mass()) } // gray, sievert: J/kg = L²·T⁻²
292    pub fn radioactivity() -> Dimension { Self::frequency() } // becquerel: decays per second = T⁻¹
293    pub fn exposure() -> Dimension { Self::charge().div(Self::mass()) } // roentgen: C/kg
294    pub fn catalytic_activity() -> Dimension { Self::amount().div(Self::time()) } // katal: mol/s
295    // Photometry.
296    pub fn luminous_flux() -> Dimension { Self::luminous().mul(Self::solid_angle()) } // lumen: cd·sr
297    pub fn illuminance() -> Dimension { Self::luminous_flux().div(Self::area()) } // lux: lm/m²
298    pub fn luminance() -> Dimension { Self::luminous().div(Self::area()) } // nit: cd/m²
299    // Rate / flow / concentration / viscosity / efficiency.
300    pub fn data_rate() -> Dimension { Self::information().div(Self::time()) } // bit/s
301    pub fn volumetric_flow() -> Dimension { Self::volume().div(Self::time()) } // m³/s
302    pub fn molar_concentration() -> Dimension { Self::amount().div(Self::volume()) } // mol/m³
303    pub fn molality() -> Dimension { Self::amount().div(Self::mass()) } // mol/kg
304    pub fn dynamic_viscosity() -> Dimension { Self::pressure().mul(Self::time()) } // Pa·s = M·L⁻¹·T⁻¹
305    pub fn kinematic_viscosity() -> Dimension { Self::area().div(Self::time()) } // m²/s = L²·T⁻¹
306    pub fn fuel_economy() -> Dimension { Self::length().div(Self::volume()) } // distance/volume (mpg) = L⁻²
307
308    /// Resolve a **named dimension** (case-insensitive) to its exponent vector — the lookup behind
309    /// the `Quantity of <Dimension>` type annotation (`Quantity of Length`, `Quantity of Area`). The
310    /// names are the human dimension words, not units; `None` for an unknown name.
311    pub fn by_name(name: &str) -> Option<Dimension> {
312        let n = name.trim().to_ascii_lowercase();
313        Some(match n.as_str() {
314            "dimensionless" | "scalar" | "number" => Dimension::DIMENSIONLESS,
315            // SI base dimensions.
316            "length" | "distance" => Self::length(),
317            "mass" => Self::mass(),
318            "time" | "duration" => Self::time(),
319            "current" | "electriccurrent" | "electric current" => Self::current(),
320            "temperature" => Self::temperature(),
321            "amount" | "amountofsubstance" => Self::amount(),
322            "luminous" | "luminousintensity" => Self::luminous(),
323            "angle" | "planeangle" => Self::angle(),
324            "solidangle" | "solid angle" => Self::solid_angle(),
325            "information" | "data" => Self::information(),
326            // Common derived dimensions.
327            "area" => Self::area(),
328            "volume" => Self::volume(),
329            "speed" | "velocity" => Self::speed(),
330            "acceleration" => Self::acceleration(),
331            "frequency" => Self::frequency(),
332            "force" | "weight" => Self::force(),
333            "energy" | "work" | "heat" => Self::energy(),
334            "power" => Self::power(),
335            "pressure" | "stress" => Self::pressure(),
336            "charge" | "electriccharge" => Self::charge(),
337            "voltage" | "potential" | "emf" => Self::voltage(),
338            "resistance" => Self::resistance(),
339            "conductance" => Self::conductance(),
340            "capacitance" => Self::capacitance(),
341            "inductance" => Self::inductance(),
342            "magneticflux" | "magnetic flux" => Self::magnetic_flux(),
343            "magneticfluxdensity" | "fluxdensity" => Self::magnetic_flux_density(),
344            "density" => Self::density(),
345            "surfacetension" | "surface tension" => Self::surface_tension(),
346            "datarate" | "data rate" | "bandwidth" => Self::data_rate(),
347            "flow" | "volumetricflow" | "volumetric flow" => Self::volumetric_flow(),
348            "concentration" | "molarconcentration" | "molarity" => Self::molar_concentration(),
349            "molality" => Self::molality(),
350            "absorbeddose" | "dose" => Self::absorbed_dose(),
351            "radioactivity" | "activity" => Self::radioactivity(),
352            "exposure" => Self::exposure(),
353            "catalyticactivity" | "catalysis" => Self::catalytic_activity(),
354            "luminousflux" => Self::luminous_flux(),
355            "illuminance" => Self::illuminance(),
356            "luminance" => Self::luminance(),
357            "fueleconomy" | "fuel economy" => Self::fuel_economy(),
358            _ => return None,
359        })
360    }
361}
362
363impl fmt::Display for Dimension {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        if self.is_dimensionless() {
366            return write!(f, "1");
367        }
368        let mut first = true;
369        for d in BaseDim::ALL {
370            let x = self.exponent(d);
371            if x.is_zero() {
372                continue;
373            }
374            if !first {
375                write!(f, "·")?;
376            }
377            first = false;
378            if x == Exp::ONE {
379                write!(f, "{}", d.symbol())?;
380            } else {
381                write!(f, "{}^{}", d.symbol(), x)?;
382            }
383        }
384        Ok(())
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn dimension_by_name_resolves_the_named_dimensions() {
394        assert_eq!(Dimension::by_name("Length"), Some(Dimension::length()));
395        assert_eq!(Dimension::by_name("length"), Some(Dimension::length())); // case-insensitive
396        assert_eq!(Dimension::by_name("Mass"), Some(Dimension::mass()));
397        assert_eq!(Dimension::by_name("Area"), Some(Dimension::area()));
398        assert_eq!(Dimension::by_name("Volume"), Some(Dimension::volume()));
399        assert_eq!(Dimension::by_name("Speed"), Some(Dimension::speed()));
400        assert_eq!(Dimension::by_name("Velocity"), Some(Dimension::speed())); // alias
401        assert_eq!(Dimension::by_name("Force"), Some(Dimension::force()));
402        assert_eq!(Dimension::by_name("Energy"), Some(Dimension::energy()));
403        // Area and Volume are distinct (L² vs L³) — the whole point.
404        assert_ne!(Dimension::by_name("Area"), Dimension::by_name("Volume"));
405        assert_ne!(Dimension::by_name("Length"), Dimension::by_name("Mass"));
406        assert_eq!(Dimension::by_name("Frobnicate"), None);
407    }
408
409    /// A tiny deterministic RNG (SplitMix64) — reproducible fuzz with no dependency.
410    struct Rng(u64);
411    impl Rng {
412        fn next(&mut self) -> u64 {
413            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
414            let mut z = self.0;
415            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
416            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
417            z ^ (z >> 31)
418        }
419        /// A random dimension with small integer exponents in [−3, 3] on each axis.
420        fn dim(&mut self) -> Dimension {
421            let mut e = [Exp::ZERO; BaseDim::COUNT];
422            for slot in e.iter_mut() {
423                *slot = Exp::int((self.next() % 7) as i32 - 3);
424            }
425            Dimension { e }
426        }
427    }
428
429    // ----------------------------------------------------------------
430    // Exp — the rational exponent
431    // ----------------------------------------------------------------
432
433    #[test]
434    fn exp_reduces_and_normalizes_sign() {
435        assert_eq!(Exp::new(2, 4), Exp::new(1, 2));
436        assert_eq!(Exp::new(-2, 4), Exp::new(1, -2)); // sign moves onto the numerator
437        assert_eq!(Exp::new(0, 5), Exp::ZERO);
438        assert_eq!(Exp::new(6, 3), Exp::int(2));
439        assert!(!Exp::new(1, 2).denominator().is_negative());
440    }
441
442    #[test]
443    fn exp_arithmetic_is_exact_rational() {
444        assert_eq!(Exp::new(1, 2).add(Exp::new(1, 2)), Exp::ONE); // ½ + ½ = 1
445        assert_eq!(Exp::new(1, 3).add(Exp::new(1, 3)).add(Exp::new(1, 3)), Exp::ONE);
446        assert_eq!(Exp::int(2).sub(Exp::new(1, 2)), Exp::new(3, 2));
447        assert_eq!(Exp::new(1, 2).scale(4), Exp::int(2));
448        assert_eq!(Exp::int(2).div_int(2), Exp::ONE); // √ of L² exponent
449        assert_eq!(Exp::ONE.div_int(2), Exp::new(1, 2)); // √ of L exponent = L^½
450        assert_eq!(Exp::new(2, 3).neg(), Exp::new(-2, 3));
451    }
452
453    #[test]
454    fn exp_displays_as_int_or_fraction() {
455        assert_eq!(Exp::int(2).to_string(), "2");
456        assert_eq!(Exp::int(-3).to_string(), "-3");
457        assert_eq!(Exp::new(1, 2).to_string(), "1/2");
458        assert_eq!(Exp::new(-1, 2).to_string(), "-1/2");
459    }
460
461    // ----------------------------------------------------------------
462    // Dimension — algebra
463    // ----------------------------------------------------------------
464
465    #[test]
466    fn base_dimensions_are_distinct() {
467        let bases: Vec<Dimension> = BaseDim::ALL.iter().map(|&d| Dimension::base(d)).collect();
468        for (i, a) in bases.iter().enumerate() {
469            for (j, b) in bases.iter().enumerate() {
470                assert_eq!(a == b, i == j, "base {i} vs {j}");
471            }
472            assert!(!a.is_dimensionless());
473        }
474        assert!(Dimension::DIMENSIONLESS.is_dimensionless());
475    }
476
477    #[test]
478    fn multiplication_adds_exponents() {
479        // Length × Length = Length² = Area.
480        assert_eq!(Dimension::length().mul(Dimension::length()), Dimension::area());
481        assert_eq!(Dimension::length().powi(3), Dimension::volume());
482        // Area × Length = Volume.
483        assert_eq!(Dimension::area().mul(Dimension::length()), Dimension::volume());
484    }
485
486    #[test]
487    fn division_subtracts_exponents() {
488        // Area ÷ Length = Length; Length ÷ Length = dimensionless.
489        assert_eq!(Dimension::area().div(Dimension::length()), Dimension::length());
490        assert!(Dimension::length().div(Dimension::length()).is_dimensionless());
491        // Speed = Length / Time.
492        assert_eq!(Dimension::length().div(Dimension::time()), Dimension::speed());
493    }
494
495    #[test]
496    fn reciprocal_is_the_group_inverse() {
497        // Frequency = 1 / Time.
498        assert_eq!(Dimension::time().recip(), Dimension::frequency());
499        // x · x⁻¹ = 1 for every base dimension.
500        for d in BaseDim::ALL {
501            let x = Dimension::base(d);
502            assert!(x.mul(x.recip()).is_dimensionless());
503        }
504        // double reciprocal is the identity.
505        assert_eq!(Dimension::force().recip().recip(), Dimension::force());
506    }
507
508    #[test]
509    fn powers_and_roots_are_inverse() {
510        assert!(Dimension::length().powi(0).is_dimensionless());
511        assert_eq!(Dimension::length().powi(-1), Dimension::length().recip());
512        // √(Area) = Length; the cube root of Volume = Length.
513        assert_eq!(Dimension::area().nth_root(2), Dimension::length());
514        assert_eq!(Dimension::volume().nth_root(3), Dimension::length());
515        // A fractional dimension survives: √(Frequency) = T^(−1/2) (noise-density shape).
516        let root_hz = Dimension::frequency().nth_root(2);
517        assert_eq!(root_hz.exponent(BaseDim::Time), Exp::new(-1, 2));
518        assert_eq!(root_hz.powi(2), Dimension::frequency());
519    }
520
521    #[test]
522    fn derived_dimensions_compose_correctly() {
523        // Force = M·L·T⁻²  (mass × acceleration).
524        let force = Dimension::force();
525        assert_eq!(force.exponent(BaseDim::Mass), Exp::int(1));
526        assert_eq!(force.exponent(BaseDim::Length), Exp::int(1));
527        assert_eq!(force.exponent(BaseDim::Time), Exp::int(-2));
528        // Energy = M·L²·T⁻² = Force · Length.
529        let energy = Dimension::energy();
530        assert_eq!(energy, Dimension::force().mul(Dimension::length()));
531        assert_eq!(energy.exponent(BaseDim::Length), Exp::int(2));
532        assert_eq!(energy.exponent(BaseDim::Time), Exp::int(-2));
533        // Power = Energy / Time = M·L²·T⁻³.
534        assert_eq!(Dimension::power().exponent(BaseDim::Time), Exp::int(-3));
535        // Pressure = Force / Area = M·L⁻¹·T⁻².
536        let p = Dimension::pressure();
537        assert_eq!(p.exponent(BaseDim::Length), Exp::int(-1));
538        assert_eq!(p.exponent(BaseDim::Mass), Exp::int(1));
539        // Charge = I·T; Voltage = Power/Current; Resistance = Voltage/Current.
540        assert_eq!(Dimension::charge(), Dimension::current().mul(Dimension::time()));
541        assert_eq!(Dimension::resistance(), Dimension::voltage().div(Dimension::current()));
542        // Density = M·L⁻³.
543        assert_eq!(Dimension::density().exponent(BaseDim::Length), Exp::int(-3));
544    }
545
546    #[test]
547    fn all_extended_derived_dimensions_have_their_canonical_signatures() {
548        use BaseDim::*;
549        // A derived dimension's exponent vector must match its physical definition exactly. Each
550        // entry: (dimension, [(axis, exponent), …]); every unlisted axis must be zero.
551        let cases: &[(Dimension, &[(BaseDim, Exp)])] = &[
552            // Radiation & catalysis.
553            (Dimension::absorbed_dose(), &[(Length, Exp::int(2)), (Time, Exp::int(-2))]), // Gy = J/kg = L²·T⁻²
554            (Dimension::radioactivity(), &[(Time, Exp::int(-1))]),                        // Bq = T⁻¹
555            (Dimension::exposure(), &[(Current, Exp::int(1)), (Time, Exp::int(1)), (Mass, Exp::int(-1))]), // C/kg
556            (Dimension::catalytic_activity(), &[(Amount, Exp::int(1)), (Time, Exp::int(-1))]),             // kat = mol/s
557            // Photometry.
558            (Dimension::luminous_flux(), &[(Luminous, Exp::int(1)), (SolidAngle, Exp::int(1))]),           // lm = cd·sr
559            (Dimension::illuminance(), &[(Luminous, Exp::int(1)), (SolidAngle, Exp::int(1)), (Length, Exp::int(-2))]), // lx
560            (Dimension::luminance(), &[(Luminous, Exp::int(1)), (Length, Exp::int(-2))]),                  // nit = cd/m²
561            // Rates, flow, concentration, viscosity, fuel economy.
562            (Dimension::data_rate(), &[(Information, Exp::int(1)), (Time, Exp::int(-1))]),                 // bit/s
563            (Dimension::volumetric_flow(), &[(Length, Exp::int(3)), (Time, Exp::int(-1))]),                // m³/s
564            (Dimension::molar_concentration(), &[(Amount, Exp::int(1)), (Length, Exp::int(-3))]),          // mol/m³
565            (Dimension::molality(), &[(Amount, Exp::int(1)), (Mass, Exp::int(-1))]),                       // mol/kg
566            (Dimension::dynamic_viscosity(), &[(Mass, Exp::int(1)), (Length, Exp::int(-1)), (Time, Exp::int(-1))]), // Pa·s
567            (Dimension::kinematic_viscosity(), &[(Length, Exp::int(2)), (Time, Exp::int(-1))]),            // m²/s
568            (Dimension::fuel_economy(), &[(Length, Exp::int(-2))]),                                        // m/m³ = L⁻²
569        ];
570        for (dim, expected) in cases {
571            for axis in BaseDim::ALL {
572                let want = expected.iter().find(|(a, _)| *a == axis).map(|(_, e)| *e).unwrap_or(Exp::int(0));
573                assert_eq!(dim.exponent(axis), want, "{} exponent on {:?}", dim, axis);
574            }
575        }
576        // Cross-checks against the composition algebra: each derived dim equals the operation that
577        // defines it, so the named helpers can never drift from their formulas.
578        assert_eq!(Dimension::absorbed_dose(), Dimension::energy().div(Dimension::mass()));
579        assert_eq!(Dimension::radioactivity(), Dimension::frequency()); // Bq is dimensionally a frequency
580        assert_eq!(Dimension::data_rate(), Dimension::information().div(Dimension::time()));
581        assert_eq!(Dimension::volumetric_flow(), Dimension::volume().div(Dimension::time()));
582        assert_eq!(Dimension::molar_concentration(), Dimension::amount().div(Dimension::volume()));
583        assert_eq!(Dimension::dynamic_viscosity(), Dimension::pressure().mul(Dimension::time()));
584        assert_eq!(Dimension::kinematic_viscosity(), Dimension::area().div(Dimension::time()));
585        assert_eq!(Dimension::fuel_economy(), Dimension::length().div(Dimension::volume()));
586        assert_eq!(Dimension::illuminance(), Dimension::luminous_flux().div(Dimension::area()));
587        // Kinematic viscosity = dynamic viscosity / density (the physical relation μ/ρ = ν).
588        assert_eq!(Dimension::kinematic_viscosity(), Dimension::dynamic_viscosity().div(Dimension::density()));
589        // Electromagnetism: each derived EM dimension equals its defining SI relation.
590        assert_eq!(Dimension::conductance(), Dimension::resistance().recip()); // S = Ω⁻¹
591        assert!(Dimension::conductance().mul(Dimension::resistance()).is_dimensionless()); // S·Ω = 1
592        assert_eq!(Dimension::capacitance(), Dimension::charge().div(Dimension::voltage())); // F = C/V
593        assert_eq!(Dimension::magnetic_flux(), Dimension::voltage().mul(Dimension::time())); // Wb = V·s
594        assert_eq!(Dimension::inductance(), Dimension::magnetic_flux().div(Dimension::current())); // H = Wb/A
595        assert_eq!(Dimension::magnetic_flux_density(), Dimension::magnetic_flux().div(Dimension::area())); // T = Wb/m²
596        assert_eq!(Dimension::surface_tension(), Dimension::force().div(Dimension::length())); // N/m
597        // Energy can also be reached as charge × voltage (Q·V = J) — a cross-path consistency check.
598        assert_eq!(Dimension::energy(), Dimension::charge().mul(Dimension::voltage()));
599    }
600
601    #[test]
602    fn display_renders_the_signature() {
603        assert_eq!(Dimension::DIMENSIONLESS.to_string(), "1");
604        assert_eq!(Dimension::length().to_string(), "L");
605        assert_eq!(Dimension::area().to_string(), "L^2");
606        assert_eq!(Dimension::frequency().to_string(), "T^-1");
607        assert_eq!(Dimension::speed().to_string(), "L·T^-1");
608        // Force in canonical (L, M, T, …) order: L·M·T^-2.
609        assert_eq!(Dimension::force().to_string(), "L·M·T^-2");
610    }
611
612    #[test]
613    fn dimension_is_copy_and_hashes_by_value() {
614        use std::collections::HashSet;
615        let a = Dimension::force();
616        let b = a; // Copy — `a` is still usable below.
617        assert_eq!(a, b);
618        let mut set = HashSet::new();
619        set.insert(Dimension::force());
620        assert!(set.contains(&Dimension::mass().mul(Dimension::acceleration())));
621        assert!(!set.contains(&Dimension::energy()));
622    }
623
624    #[test]
625    fn dimensions_form_an_abelian_group_under_fuzz() {
626        // Property fuzz: random exponent vectors satisfy the abelian-group axioms exactly, and
627        // power/root/distributivity laws hold.
628        let mut r = Rng(0x_D1E5_10_4ABE_1234);
629        let id = Dimension::DIMENSIONLESS;
630        for _ in 0..4000 {
631            let (a, b, c) = (r.dim(), r.dim(), r.dim());
632            // Commutativity + associativity of ×.
633            assert_eq!(a.mul(b), b.mul(a), "× commutes");
634            assert_eq!(a.mul(b).mul(c), a.mul(b.mul(c)), "× associates");
635            // Identity + inverse.
636            assert_eq!(a.mul(id), a, "1 is the identity");
637            assert!(a.mul(a.recip()).is_dimensionless(), "x · x⁻¹ = 1");
638            // Division agrees with multiply-by-inverse.
639            assert_eq!(a.div(b), a.mul(b.recip()), "a / b = a · b⁻¹");
640            assert!(a.div(a).is_dimensionless(), "a / a = 1");
641            // Powers: aᵏ · aᵐ = aᵏ⁺ᵐ; (a·b)ᵏ = aᵏ · bᵏ; (aᵏ) root k = a.
642            assert_eq!(a.powi(2).mul(a.powi(3)), a.powi(5), "exponent law");
643            assert_eq!(a.mul(b).powi(2), a.powi(2).mul(b.powi(2)), "power distributes over ×");
644            assert_eq!(a.powi(2).nth_root(2), a, "(a²) √2 = a");
645            assert_eq!(a.powi(0), id, "a⁰ = 1");
646            // recip is an involution.
647            assert_eq!(a.recip().recip(), a);
648        }
649    }
650}