Skip to main content

oxirs_physics/
uom_quantities.rs

1//! Type-safe physics quantities via the `uom` crate (requires `simulation` feature).
2//!
3//! This module provides `UomQuantity` wrappers for common SI quantities used
4//! throughout the physics simulation bridge.  Using compile-time dimensional
5//! types eliminates unit-mismatch bugs that raw `f64` parameters cannot catch.
6//!
7//! ## Supported Quantities
8//!
9//! | Type alias | SI unit | Description |
10//! |---|---|---|
11//! | `Mass` | kg | mass |
12//! | `Velocity` | m/s | velocity |
13//! | `Acceleration` | m/s² | acceleration |
14//! | `Energy` | J (kg·m²/s²) | energy |
15//! | `Temperature` | K | thermodynamic temperature |
16//! | `Force` | N (kg·m/s²) | force |
17//! | `Pressure` | Pa (kg/(m·s²)) | pressure |
18//! | `Length` | m | length |
19//! | `Time` | s | time interval |
20//! | `AngularMomentum` | kg·m²/s | angular momentum |
21//! | `Entropy` | J/K | thermodynamic entropy |
22//!
23//! ## Example
24//!
25//! ```rust,no_run
26//! # #[cfg(feature = "simulation")] {
27//! use oxirs_physics::uom_quantities::{energy_j, mass_kg, to_joules, to_kg};
28//!
29//! let m = mass_kg(1.5);
30//! let e = energy_j(100.0);
31//!
32//! // Convert back to f64 for simulation APIs
33//! assert!((to_kg(&m) - 1.5).abs() < 1e-12);
34//! assert!((to_joules(&e) - 100.0).abs() < 1e-12);
35//! # }
36//! ```
37
38use uom::si::f64 as si;
39use uom::si::{
40    acceleration::meter_per_second_squared, angular_momentum::kilogram_square_meter_per_second,
41    energy::joule, force::newton, length::meter, mass::kilogram, pressure::pascal,
42    thermodynamic_temperature::kelvin as temperature_kelvin, time::second,
43    velocity::meter_per_second,
44};
45
46// ─────────────────────────────────────────────────────────────────────────────
47// Type aliases for common SI quantities
48// ─────────────────────────────────────────────────────────────────────────────
49
50/// Mass in SI kilograms.
51pub type Mass = si::Mass;
52
53/// Length in SI metres.
54pub type Length = si::Length;
55
56/// Time in SI seconds.
57pub type Time = si::Time;
58
59/// Velocity in SI metres per second.
60pub type Velocity = si::Velocity;
61
62/// Acceleration in SI metres per second squared.
63pub type Acceleration = si::Acceleration;
64
65/// Force in SI Newtons (kg·m/s²).
66pub type Force = si::Force;
67
68/// Pressure in SI Pascals (kg/(m·s²)).
69pub type Pressure = si::Pressure;
70
71/// Energy in SI Joules (kg·m²/s²).
72pub type Energy = si::Energy;
73
74/// Thermodynamic temperature in SI Kelvin.
75pub type ThermodynamicTemperature = si::ThermodynamicTemperature;
76
77/// Angular momentum in kg·m²/s.
78pub type AngularMomentum = si::AngularMomentum;
79
80// ─────────────────────────────────────────────────────────────────────────────
81// Entropy (J/K) — represented as Energy/TemperatureInterval
82// ─────────────────────────────────────────────────────────────────────────────
83
84/// Entropy in J/K, represented as a `f64` with explicit units.
85///
86/// Because `uom` does not directly expose an `Entropy` quantity as a standalone
87/// type in all configurations, this module uses a thin wrapper.
88#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
89pub struct Entropy {
90    /// Raw value in J/K.
91    value_jk: f64,
92}
93
94impl Entropy {
95    /// Create an entropy from a value in J/K.
96    pub fn from_joules_per_kelvin(jk: f64) -> Self {
97        Self { value_jk: jk }
98    }
99
100    /// Return the value in J/K.
101    pub fn get_joules_per_kelvin(self) -> f64 {
102        self.value_jk
103    }
104
105    /// Compute entropy change for a reversible process: ΔS = Q_rev / T.
106    ///
107    /// Returns `None` if temperature is zero or negative.
108    pub fn reversible_change(
109        heat: &Energy,
110        temperature: &ThermodynamicTemperature,
111    ) -> Option<Self> {
112        let t = temperature.get::<temperature_kelvin>();
113        if t <= 0.0 {
114            return None;
115        }
116        Some(Self::from_joules_per_kelvin(heat.get::<joule>() / t))
117    }
118}
119
120impl std::ops::Add for Entropy {
121    type Output = Self;
122    fn add(self, rhs: Self) -> Self {
123        Self::from_joules_per_kelvin(self.value_jk + rhs.value_jk)
124    }
125}
126
127impl std::ops::Sub for Entropy {
128    type Output = Self;
129    fn sub(self, rhs: Self) -> Self {
130        Self::from_joules_per_kelvin(self.value_jk - rhs.value_jk)
131    }
132}
133
134// ─────────────────────────────────────────────────────────────────────────────
135// Convenience constructors
136// ─────────────────────────────────────────────────────────────────────────────
137
138/// Create a [`Mass`] from kilograms.
139pub fn mass_kg(kg: f64) -> Mass {
140    si::Mass::new::<kilogram>(kg)
141}
142
143/// Create a [`Length`] from metres.
144pub fn length_m(m: f64) -> Length {
145    si::Length::new::<meter>(m)
146}
147
148/// Create a [`Time`] from seconds.
149pub fn time_s(s: f64) -> Time {
150    si::Time::new::<second>(s)
151}
152
153/// Create a [`Velocity`] from metres per second.
154pub fn velocity_ms(ms: f64) -> Velocity {
155    si::Velocity::new::<meter_per_second>(ms)
156}
157
158/// Create an [`Acceleration`] from metres per second squared.
159pub fn acceleration_ms2(a: f64) -> Acceleration {
160    si::Acceleration::new::<meter_per_second_squared>(a)
161}
162
163/// Create a [`Force`] from Newtons.
164pub fn force_n(n: f64) -> Force {
165    si::Force::new::<newton>(n)
166}
167
168/// Create a [`Pressure`] from Pascals.
169pub fn pressure_pa(pa: f64) -> Pressure {
170    si::Pressure::new::<pascal>(pa)
171}
172
173/// Create an [`Energy`] from Joules.
174pub fn energy_j(j: f64) -> Energy {
175    si::Energy::new::<joule>(j)
176}
177
178/// Create a [`ThermodynamicTemperature`] from Kelvin.
179pub fn temperature_k(k: f64) -> ThermodynamicTemperature {
180    si::ThermodynamicTemperature::new::<temperature_kelvin>(k)
181}
182
183/// Create an [`AngularMomentum`] from kg·m²/s.
184pub fn angular_momentum(l: f64) -> AngularMomentum {
185    si::AngularMomentum::new::<kilogram_square_meter_per_second>(l)
186}
187
188// ─────────────────────────────────────────────────────────────────────────────
189// Value extractors (→ f64 in SI base units)
190// ─────────────────────────────────────────────────────────────────────────────
191
192/// Extract mass in kg.
193pub fn to_kg(m: &Mass) -> f64 {
194    m.get::<kilogram>()
195}
196
197/// Extract length in m.
198pub fn to_m(l: &Length) -> f64 {
199    l.get::<meter>()
200}
201
202/// Extract time in s.
203pub fn to_s(t: &Time) -> f64 {
204    t.get::<second>()
205}
206
207/// Extract velocity in m/s.
208pub fn to_ms(v: &Velocity) -> f64 {
209    v.get::<meter_per_second>()
210}
211
212/// Extract acceleration in m/s².
213pub fn to_ms2(a: &Acceleration) -> f64 {
214    a.get::<meter_per_second_squared>()
215}
216
217/// Extract force in N.
218pub fn to_n(f: &Force) -> f64 {
219    f.get::<newton>()
220}
221
222/// Extract pressure in Pa.
223pub fn to_pa(p: &Pressure) -> f64 {
224    p.get::<pascal>()
225}
226
227/// Extract energy in J.
228pub fn to_joules(e: &Energy) -> f64 {
229    e.get::<joule>()
230}
231
232/// Extract temperature in K.
233pub fn to_kelvin(t: &ThermodynamicTemperature) -> f64 {
234    t.get::<temperature_kelvin>()
235}
236
237/// Extract angular momentum in kg·m²/s.
238pub fn to_angular_momentum(l: &AngularMomentum) -> f64 {
239    l.get::<kilogram_square_meter_per_second>()
240}
241
242// ─────────────────────────────────────────────────────────────────────────────
243// Kinetic energy helper
244// ─────────────────────────────────────────────────────────────────────────────
245
246/// Compute kinetic energy: KE = ½mv².
247pub fn kinetic_energy(mass: &Mass, velocity: &Velocity) -> Energy {
248    let ke_j = 0.5 * to_kg(mass) * to_ms(velocity).powi(2);
249    energy_j(ke_j)
250}
251
252/// Compute gravitational potential energy: PE = mgh.
253pub fn gravitational_pe(mass: &Mass, height: &Length, g: f64) -> Energy {
254    energy_j(to_kg(mass) * g * to_m(height))
255}
256
257// ─────────────────────────────────────────────────────────────────────────────
258// Tests
259// ─────────────────────────────────────────────────────────────────────────────
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_mass_round_trip() {
267        let m = mass_kg(2.5);
268        assert!((to_kg(&m) - 2.5).abs() < 1e-12);
269    }
270
271    #[test]
272    fn test_energy_round_trip() {
273        let e = energy_j(1000.0);
274        assert!((to_joules(&e) - 1000.0).abs() < 1e-12);
275    }
276
277    #[test]
278    fn test_temperature_round_trip() {
279        let t = temperature_k(300.0);
280        assert!((to_kelvin(&t) - 300.0).abs() < 1e-12);
281    }
282
283    #[test]
284    fn test_kinetic_energy_formula() {
285        // KE = 0.5 * 2 kg * (3 m/s)^2 = 9 J
286        let m = mass_kg(2.0);
287        let v = velocity_ms(3.0);
288        let ke = kinetic_energy(&m, &v);
289        assert!((to_joules(&ke) - 9.0).abs() < 1e-10);
290    }
291
292    #[test]
293    fn test_gravitational_pe_formula() {
294        // PE = 1 kg * 9.81 m/s^2 * 10 m = 98.1 J
295        let m = mass_kg(1.0);
296        let h = length_m(10.0);
297        let pe = gravitational_pe(&m, &h, 9.81);
298        assert!((to_joules(&pe) - 98.1).abs() < 1e-10);
299    }
300
301    #[test]
302    fn test_entropy_from_heat_and_temperature() {
303        // ΔS = Q/T = 300 J / 300 K = 1 J/K
304        let q = energy_j(300.0);
305        let t = temperature_k(300.0);
306        let ds = Entropy::reversible_change(&q, &t).unwrap();
307        assert!((ds.get_joules_per_kelvin() - 1.0).abs() < 1e-10);
308    }
309
310    #[test]
311    fn test_entropy_zero_temperature_returns_none() {
312        let q = energy_j(100.0);
313        let t = temperature_k(0.0);
314        assert!(Entropy::reversible_change(&q, &t).is_none());
315    }
316
317    #[test]
318    fn test_entropy_addition() {
319        let s1 = Entropy::from_joules_per_kelvin(2.0);
320        let s2 = Entropy::from_joules_per_kelvin(3.0);
321        let s3 = s1 + s2;
322        assert!((s3.get_joules_per_kelvin() - 5.0).abs() < 1e-12);
323    }
324
325    #[test]
326    fn test_angular_momentum_round_trip() {
327        let l = angular_momentum(4.2);
328        assert!((to_angular_momentum(&l) - 4.2).abs() < 1e-12);
329    }
330
331    #[test]
332    fn test_si_unit_mismatch_would_not_compile() {
333        // This test documents the type safety guarantee:
334        // Uncommenting the line below would cause a compile error:
335        // let _: Mass = energy_j(1.0); // type mismatch — Energy != Mass
336        // Type safety is guaranteed at compile time; no runtime assertion needed.
337        let _ = (); // marker: compile-time type safety verified
338    }
339}