Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_units/src/dimension.rs

10.8 KiB, 1 run

created by r1870400018:17293, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Dimensional algebra over the SI base dimensions.
2//!
3//! A [`Dimension`] is a vector of rational exponents, one per base dimension.
4//! Multiplying two dimensioned quantities adds their exponents, dividing
5//! subtracts them, and raising to a power scales them. Two dimensions are
6//! equal only when every exponent matches, which is what lets a dimensioned
7//! value reject an addition that would violate physics.
8//!
9//! The seven SI base dimensions are length, mass, time, electric current,
10//! thermodynamic temperature, amount of substance and luminous intensity. A
11//! plane angle (radian) is not an SI base dimension, but it is carried here as
12//! an eighth, tagged slot so that an angle cannot be silently added to a bare
13//! dimensionless number.
14
15use oxedyne_fe2o3_core::prelude::*;
16
17/// Greatest common divisor of two integers, always non-negative and at least
18/// one so that it is safe to divide by.
19fn gcd(a: i32, b: i32) -> i32 {
20 let mut a = a.abs();
21 let mut b = b.abs();
22 while b != 0 {
23 let t = b; // Remember divisor.
24 b = a % b;
25 a = t;
26 }
27 if a == 0 { 1 } else { a }
28}
29
30/// A rational exponent, always stored in reduced form with a strictly positive
31/// denominator. Rational (rather than integer) exponents allow roots, so that
32/// the square root of an area is a length.
33#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
34pub struct Ratio {
35 num: i32, // Numerator.
36 den: i32, // Denominator, always > 0.
37}
38
39impl Ratio {
40
41 /// The rational zero, `0/1`.
42 pub const ZERO: Self = Self { num: 0, den: 1 };
43
44 /// The rational one, `1/1`.
45 pub const ONE: Self = Self { num: 1, den: 1 };
46
47 /// Creates a reduced ratio from an integer.
48 pub fn int(n: i32) -> Self {
49 Self { num: n, den: 1 }
50 }
51
52 /// Creates a reduced ratio from a numerator and denominator, erroring on a
53 /// zero denominator.
54 pub fn frac(num: i32, den: i32) -> Outcome<Self> {
55 if den == 0 {
56 return Err(err!(
57 "A rational exponent cannot have a zero denominator.";
58 Input, Invalid));
59 }
60 Ok(Self::reduce(num, den))
61 }
62
63 /// Reduces a numerator and denominator to lowest terms with a positive
64 /// denominator. The denominator is assumed to be non-zero.
65 fn reduce(mut num: i32, mut den: i32) -> Self {
66 if den < 0 {
67 num = -num; // Keep the sign on the numerator.
68 den = -den;
69 }
70 let g = gcd(num, den);
71 Self { num: num / g, den: den / g }
72 }
73
74 /// Returns the numerator.
75 pub fn num(&self) -> i32 { self.num }
76
77 /// Returns the denominator.
78 pub fn den(&self) -> i32 { self.den }
79
80 /// Returns `true` if the ratio is zero.
81 pub fn is_zero(&self) -> bool { self.num == 0 }
82
83 /// Adds two ratios.
84 pub fn add(&self, other: &Self) -> Self {
85 Self::reduce(
86 self.num * other.den + other.num * self.den,
87 self.den * other.den,
88 )
89 }
90
91 /// Subtracts one ratio from another.
92 pub fn sub(&self, other: &Self) -> Self {
93 Self::reduce(
94 self.num * other.den - other.num * self.den,
95 self.den * other.den,
96 )
97 }
98
99 /// Multiplies two ratios, used when raising a dimension to a rational
100 /// power.
101 pub fn mul(&self, other: &Self) -> Self {
102 Self::reduce(self.num * other.num, self.den * other.den)
103 }
104
105 /// Returns the exponent as a floating point value, used when raising a
106 /// magnitude to this power.
107 pub fn as_f64(&self) -> f64 {
108 (self.num as f64) / (self.den as f64)
109 }
110}
111
112impl std::fmt::Display for Ratio {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 if self.den == 1 {
115 write!(f, "{}", self.num)
116 } else {
117 write!(f, "{}/{}", self.num, self.den)
118 }
119 }
120}
121
122/// The base dimensions carried by a [`Dimension`] vector, in index order. The
123/// first seven are the SI base dimensions; `Angle` is a tagged plane-angle
124/// slot kept separate from them so that a radian is distinct from a bare
125/// number.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum Base {
128 Length, // metre, m
129 Mass, // kilogram, kg
130 Time, // second, s
131 Current, // ampere, A
132 Temperature, // kelvin, K
133 Amount, // mole, mol
134 LuminousIntensity, // candela, cd
135 Angle, // radian, rad (tagged, non-SI)
136}
137
138impl Base {
139
140 /// The number of base slots, seven SI dimensions plus the tagged angle.
141 pub const COUNT: usize = 8;
142
143 /// The base dimensions in index order.
144 pub const ALL: [Base; Self::COUNT] = [
145 Base::Length,
146 Base::Mass,
147 Base::Time,
148 Base::Current,
149 Base::Temperature,
150 Base::Amount,
151 Base::LuminousIntensity,
152 Base::Angle,
153 ];
154
155 /// Returns the index of this base within a dimension vector.
156 pub fn index(&self) -> usize {
157 match self {
158 Base::Length => 0,
159 Base::Mass => 1,
160 Base::Time => 2,
161 Base::Current => 3,
162 Base::Temperature => 4,
163 Base::Amount => 5,
164 Base::LuminousIntensity => 6,
165 Base::Angle => 7,
166 }
167 }
168
169 /// Returns the SI symbol for this base dimension.
170 pub fn symbol(&self) -> &'static str {
171 match self {
172 Base::Length => "m",
173 Base::Mass => "kg",
174 Base::Time => "s",
175 Base::Current => "A",
176 Base::Temperature => "K",
177 Base::Amount => "mol",
178 Base::LuminousIntensity => "cd",
179 Base::Angle => "rad",
180 }
181 }
182}
183
184/// A physical dimension expressed as rational exponents over the base
185/// dimensions.
186///
187/// Construction is arbitrary: any combination of base exponents is a valid
188/// dimension. Dimensions form a group under multiplication (exponent addition),
189/// with [`Dimension::dimensionless`] as the identity.
190#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
191pub struct Dimension {
192 exp: [Ratio; Base::COUNT],
193}
194
195impl Dimension {
196
197 /// Creates a dimension from a full vector of rational exponents.
198 pub fn new(exp: [Ratio; Base::COUNT]) -> Self {
199 Self { exp }
200 }
201
202 /// The dimensionless quantity, all exponents zero. This is the identity for
203 /// multiplication.
204 pub fn dimensionless() -> Self {
205 Self { exp: [Ratio::ZERO; Base::COUNT] }
206 }
207
208 /// Creates a dimension that is a single base raised to the first power.
209 pub fn base(b: Base) -> Self {
210 let mut exp = [Ratio::ZERO; Base::COUNT];
211 exp[b.index()] = Ratio::ONE;
212 Self { exp }
213 }
214
215 /// The exponent of a given base dimension.
216 pub fn exponent(&self, b: Base) -> Ratio {
217 self.exp[b.index()]
218 }
219
220 /// Returns `true` if every exponent is zero, i.e. the quantity is
221 /// dimensionless.
222 pub fn is_dimensionless(&self) -> bool {
223 self.exp.iter().all(|r| r.is_zero())
224 }
225
226 /// Multiplies two dimensions by adding their exponents.
227 pub fn mul(&self, other: &Self) -> Self {
228 let mut exp = [Ratio::ZERO; Base::COUNT];
229 for i in 0..Base::COUNT {
230 exp[i] = self.exp[i].add(&other.exp[i]);
231 }
232 Self { exp }
233 }
234
235 /// Divides one dimension by another by subtracting exponents.
236 pub fn div(&self, other: &Self) -> Self {
237 let mut exp = [Ratio::ZERO; Base::COUNT];
238 for i in 0..Base::COUNT {
239 exp[i] = self.exp[i].sub(&other.exp[i]);
240 }
241 Self { exp }
242 }
243
244 /// Raises a dimension to an integer power, scaling every exponent.
245 pub fn powi(&self, n: i32) -> Self {
246 self.pow(&Ratio::int(n))
247 }
248
249 /// Raises a dimension to a rational power, scaling every exponent. This is
250 /// how a root reduces a dimension, e.g. the square root of an area (`L^2`)
251 /// is a length (`L`).
252 pub fn pow(&self, p: &Ratio) -> Self {
253 let mut exp = [Ratio::ZERO; Base::COUNT];
254 for i in 0..Base::COUNT {
255 exp[i] = self.exp[i].mul(p);
256 }
257 Self { exp }
258 }
259
260 /// Returns the reciprocal dimension, negating every exponent.
261 pub fn recip(&self) -> Self {
262 Self::dimensionless().div(self)
263 }
264
265 // Convenience constructors for the base dimensions. //
266
267 /// Length, `L`.
268 pub fn length() -> Self { Self::base(Base::Length) }
269 /// Mass, `M`.
270 pub fn mass() -> Self { Self::base(Base::Mass) }
271 /// Time, `T`.
272 pub fn time() -> Self { Self::base(Base::Time) }
273 /// Electric current, `I`.
274 pub fn current() -> Self { Self::base(Base::Current) }
275 /// Thermodynamic temperature, `Θ`.
276 pub fn temperature() -> Self { Self::base(Base::Temperature) }
277 /// Amount of substance, `N`.
278 pub fn amount() -> Self { Self::base(Base::Amount) }
279 /// Luminous intensity, `J`.
280 pub fn luminous_intensity() -> Self { Self::base(Base::LuminousIntensity) }
281 /// Plane angle, tagged and distinct from dimensionless.
282 pub fn angle() -> Self { Self::base(Base::Angle) }
283
284 // Convenience constructors for common derived dimensions. //
285
286 /// Area, `L^2`.
287 pub fn area() -> Self { Self::length().powi(2) }
288 /// Volume, `L^3`.
289 pub fn volume() -> Self { Self::length().powi(3) }
290 /// Velocity, `L·T^-1`.
291 pub fn velocity() -> Self { Self::length().div(&Self::time()) }
292 /// Acceleration, `L·T^-2`.
293 pub fn acceleration() -> Self { Self::velocity().div(&Self::time()) }
294 /// Force, `M·L·T^-2`.
295 pub fn force() -> Self { Self::mass().mul(&Self::acceleration()) }
296 /// Energy, `M·L^2·T^-2`.
297 pub fn energy() -> Self { Self::force().mul(&Self::length()) }
298 /// Power, `M·L^2·T^-3`.
299 pub fn power() -> Self { Self::energy().div(&Self::time()) }
300 /// Frequency, `T^-1`.
301 pub fn frequency() -> Self { Self::dimensionless().div(&Self::time()) }
302}
303
304impl std::ops::Mul for Dimension {
305 type Output = Dimension;
306 fn mul(self, other: Dimension) -> Dimension {
307 Dimension::mul(&self, &other)
308 }
309}
310
311impl std::ops::Div for Dimension {
312 type Output = Dimension;
313 fn div(self, other: Dimension) -> Dimension {
314 Dimension::div(&self, &other)
315 }
316}
317
318impl std::fmt::Display for Dimension {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 if self.is_dimensionless() {
321 return write!(f, "1");
322 }
323 let mut first = true;
324 for b in Base::ALL.iter() {
325 let e = self.exp[b.index()];
326 if e.is_zero() {
327 continue;
328 }
329 if !first {
330 ok!(write!(f, "·"));
331 }
332 first = false;
333 if e == Ratio::ONE {
334 ok!(write!(f, "{}", b.symbol()));
335 } else {
336 ok!(write!(f, "{}^{}", b.symbol(), e));
337 }
338 }
339 Ok(())
340 }
341}