oxirs_physics/constraints/
dimensional_analysis.rs

1//! Dimensional Analysis and Unit Checking
2//!
3//! Validates dimensional consistency and unit compatibility using fundamental SI dimensions
4
5use crate::error::{PhysicsError, PhysicsResult};
6use std::collections::HashMap;
7
8/// Fundamental SI dimensions (L, M, T, I, Θ, N, J)
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Dimensions {
11    /// Length (m)
12    pub length: i8,
13    /// Mass (kg)
14    pub mass: i8,
15    /// Time (s)
16    pub time: i8,
17    /// Electric current (A)
18    pub current: i8,
19    /// Temperature (K)
20    pub temperature: i8,
21    /// Amount of substance (mol)
22    pub amount: i8,
23    /// Luminous intensity (cd)
24    pub luminosity: i8,
25}
26
27impl Dimensions {
28    /// Dimensionless quantity
29    pub const fn dimensionless() -> Self {
30        Self {
31            length: 0,
32            mass: 0,
33            time: 0,
34            current: 0,
35            temperature: 0,
36            amount: 0,
37            luminosity: 0,
38        }
39    }
40
41    /// Check if dimensions are equal
42    pub fn equals(&self, other: &Self) -> bool {
43        self.length == other.length
44            && self.mass == other.mass
45            && self.time == other.time
46            && self.current == other.current
47            && self.temperature == other.temperature
48            && self.amount == other.amount
49            && self.luminosity == other.luminosity
50    }
51
52    /// Check if dimensionless
53    pub fn is_dimensionless(&self) -> bool {
54        self.equals(&Self::dimensionless())
55    }
56}
57
58/// Dimensional Analyzer
59pub struct DimensionalAnalyzer {
60    /// Known unit dimensions
61    unit_dimensions: HashMap<String, Dimensions>,
62}
63
64impl DimensionalAnalyzer {
65    pub fn new() -> Self {
66        let mut analyzer = Self {
67            unit_dimensions: HashMap::new(),
68        };
69        analyzer.initialize_si_units();
70        analyzer
71    }
72
73    /// Initialize common SI units and derived units
74    fn initialize_si_units(&mut self) {
75        // Base SI units
76        self.register_unit(
77            "m",
78            Dimensions {
79                length: 1,
80                ..Dimensions::dimensionless()
81            },
82        );
83        self.register_unit(
84            "kg",
85            Dimensions {
86                mass: 1,
87                ..Dimensions::dimensionless()
88            },
89        );
90        self.register_unit(
91            "s",
92            Dimensions {
93                time: 1,
94                ..Dimensions::dimensionless()
95            },
96        );
97        self.register_unit(
98            "A",
99            Dimensions {
100                current: 1,
101                ..Dimensions::dimensionless()
102            },
103        );
104        self.register_unit(
105            "K",
106            Dimensions {
107                temperature: 1,
108                ..Dimensions::dimensionless()
109            },
110        );
111        self.register_unit(
112            "mol",
113            Dimensions {
114                amount: 1,
115                ..Dimensions::dimensionless()
116            },
117        );
118        self.register_unit(
119            "cd",
120            Dimensions {
121                luminosity: 1,
122                ..Dimensions::dimensionless()
123            },
124        );
125
126        // Derived units - Mechanical
127        self.register_unit(
128            "N",
129            Dimensions {
130                length: 1,
131                mass: 1,
132                time: -2,
133                ..Dimensions::dimensionless()
134            },
135        ); // Newton
136        self.register_unit(
137            "Pa",
138            Dimensions {
139                length: -1,
140                mass: 1,
141                time: -2,
142                ..Dimensions::dimensionless()
143            },
144        ); // Pascal
145        self.register_unit(
146            "J",
147            Dimensions {
148                length: 2,
149                mass: 1,
150                time: -2,
151                ..Dimensions::dimensionless()
152            },
153        ); // Joule
154        self.register_unit(
155            "W",
156            Dimensions {
157                length: 2,
158                mass: 1,
159                time: -3,
160                ..Dimensions::dimensionless()
161            },
162        ); // Watt
163
164        // Derived units - Electrical
165        self.register_unit(
166            "V",
167            Dimensions {
168                length: 2,
169                mass: 1,
170                time: -3,
171                current: -1,
172                ..Dimensions::dimensionless()
173            },
174        ); // Volt
175        self.register_unit(
176            "Ω",
177            Dimensions {
178                length: 2,
179                mass: 1,
180                time: -3,
181                current: -2,
182                ..Dimensions::dimensionless()
183            },
184        ); // Ohm
185        self.register_unit(
186            "C",
187            Dimensions {
188                time: 1,
189                current: 1,
190                ..Dimensions::dimensionless()
191            },
192        ); // Coulomb
193
194        // Thermal units
195        self.register_unit(
196            "°C",
197            Dimensions {
198                temperature: 1,
199                ..Dimensions::dimensionless()
200            },
201        );
202        self.register_unit(
203            "W/(m*K)",
204            Dimensions {
205                length: 1,
206                mass: 1,
207                time: -3,
208                temperature: -1,
209                ..Dimensions::dimensionless()
210            },
211        ); // Thermal conductivity
212        self.register_unit(
213            "J/(kg*K)",
214            Dimensions {
215                length: 2,
216                time: -2,
217                temperature: -1,
218                ..Dimensions::dimensionless()
219            },
220        ); // Specific heat
221        self.register_unit(
222            "kg/m^3",
223            Dimensions {
224                length: -3,
225                mass: 1,
226                ..Dimensions::dimensionless()
227            },
228        ); // Density
229
230        // Dimensionless
231        self.register_unit("dimensionless", Dimensions::dimensionless());
232        self.register_unit("1", Dimensions::dimensionless());
233    }
234
235    /// Register a unit with its dimensions
236    pub fn register_unit(&mut self, unit: &str, dimensions: Dimensions) {
237        self.unit_dimensions.insert(unit.to_string(), dimensions);
238    }
239
240    /// Get dimensions for a unit
241    pub fn get_dimensions(&self, unit: &str) -> Option<&Dimensions> {
242        self.unit_dimensions.get(unit)
243    }
244
245    /// Check dimensional consistency between two quantities
246    pub fn check_consistency(&self, unit1: &str, unit2: &str) -> PhysicsResult<bool> {
247        let dim1 = self
248            .get_dimensions(unit1)
249            .ok_or_else(|| PhysicsError::UnitConversion(format!("Unknown unit: {}", unit1)))?;
250
251        let dim2 = self
252            .get_dimensions(unit2)
253            .ok_or_else(|| PhysicsError::UnitConversion(format!("Unknown unit: {}", unit2)))?;
254
255        Ok(dim1.equals(dim2))
256    }
257
258    /// Check if a unit is dimensionless
259    pub fn is_dimensionless(&self, unit: &str) -> PhysicsResult<bool> {
260        let dim = self
261            .get_dimensions(unit)
262            .ok_or_else(|| PhysicsError::UnitConversion(format!("Unknown unit: {}", unit)))?;
263
264        Ok(dim.is_dimensionless())
265    }
266}
267
268impl Default for DimensionalAnalyzer {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274/// Unit Checker - validates unit compatibility and conversions
275pub struct UnitChecker {
276    analyzer: DimensionalAnalyzer,
277}
278
279impl UnitChecker {
280    pub fn new() -> Self {
281        Self {
282            analyzer: DimensionalAnalyzer::new(),
283        }
284    }
285
286    /// Validate unit compatibility (same dimensions)
287    pub fn check_compatibility(&self, unit1: &str, unit2: &str) -> PhysicsResult<bool> {
288        self.analyzer.check_consistency(unit1, unit2)
289    }
290
291    /// Validate that a unit exists
292    pub fn validate_unit(&self, unit: &str) -> PhysicsResult<()> {
293        if self.analyzer.get_dimensions(unit).is_none() {
294            return Err(PhysicsError::UnitConversion(format!(
295                "Unknown or unsupported unit: {}",
296                unit
297            )));
298        }
299        Ok(())
300    }
301
302    /// Get canonical form of a unit (if available)
303    pub fn canonical_form(&self, unit: &str) -> PhysicsResult<String> {
304        // Validate unit exists
305        self.validate_unit(unit)?;
306
307        // For now, return the unit as-is
308        // Future: Convert to canonical SI form (e.g., N -> kg·m/s²)
309        Ok(unit.to_string())
310    }
311
312    /// Check if conversion is possible between units
313    pub fn can_convert(&self, from: &str, to: &str) -> PhysicsResult<bool> {
314        self.check_compatibility(from, to)
315    }
316}
317
318impl Default for UnitChecker {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_dimensional_consistency() {
330        let analyzer = DimensionalAnalyzer::new();
331
332        // Same units should be consistent
333        assert!(analyzer.check_consistency("m", "m").unwrap());
334        assert!(analyzer.check_consistency("kg", "kg").unwrap());
335
336        // Different units with same dimensions
337        // Note: m and km have different prefixes but same dimensions
338        // This would require prefix handling (future work)
339
340        // Different dimensions should not be consistent
341        assert!(!analyzer.check_consistency("m", "kg").unwrap());
342        assert!(!analyzer.check_consistency("J", "Pa").unwrap());
343    }
344
345    #[test]
346    fn test_derived_units() {
347        let analyzer = DimensionalAnalyzer::new();
348
349        // Force (N) = mass * length / time^2
350        let newton = analyzer.get_dimensions("N").unwrap();
351        assert_eq!(newton.mass, 1);
352        assert_eq!(newton.length, 1);
353        assert_eq!(newton.time, -2);
354
355        // Energy (J) = mass * length^2 / time^2
356        let joule = analyzer.get_dimensions("J").unwrap();
357        assert_eq!(joule.mass, 1);
358        assert_eq!(joule.length, 2);
359        assert_eq!(joule.time, -2);
360
361        // Power (W) = energy / time
362        let watt = analyzer.get_dimensions("W").unwrap();
363        assert_eq!(watt.mass, 1);
364        assert_eq!(watt.length, 2);
365        assert_eq!(watt.time, -3);
366    }
367
368    #[test]
369    fn test_dimensionless() {
370        let analyzer = DimensionalAnalyzer::new();
371
372        assert!(analyzer.is_dimensionless("dimensionless").unwrap());
373        assert!(analyzer.is_dimensionless("1").unwrap());
374        assert!(!analyzer.is_dimensionless("m").unwrap());
375    }
376
377    #[test]
378    fn test_unit_checker() {
379        let checker = UnitChecker::new();
380
381        // Valid units
382        assert!(checker.validate_unit("m").is_ok());
383        assert!(checker.validate_unit("kg").is_ok());
384        assert!(checker.validate_unit("N").is_ok());
385
386        // Invalid unit
387        assert!(checker.validate_unit("invalid_unit").is_err());
388
389        // Compatibility
390        assert!(checker.check_compatibility("m", "m").unwrap());
391        assert!(!checker.check_compatibility("m", "kg").unwrap());
392    }
393
394    #[test]
395    fn test_thermal_units() {
396        let analyzer = DimensionalAnalyzer::new();
397
398        // Thermal conductivity: W/(m·K) = kg·m/s³·K
399        let k = analyzer.get_dimensions("W/(m*K)").unwrap();
400        assert_eq!(k.length, 1);
401        assert_eq!(k.mass, 1);
402        assert_eq!(k.time, -3);
403        assert_eq!(k.temperature, -1);
404
405        // Specific heat: J/(kg·K) = m²/(s²·K)
406        let cp = analyzer.get_dimensions("J/(kg*K)").unwrap();
407        assert_eq!(cp.length, 2);
408        assert_eq!(cp.mass, 0);
409        assert_eq!(cp.time, -2);
410        assert_eq!(cp.temperature, -1);
411    }
412}