oxirs_physics/constraints/
dimensional_analysis.rs1use crate::error::{PhysicsError, PhysicsResult};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Dimensions {
11 pub length: i8,
13 pub mass: i8,
15 pub time: i8,
17 pub current: i8,
19 pub temperature: i8,
21 pub amount: i8,
23 pub luminosity: i8,
25}
26
27impl Dimensions {
28 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 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 pub fn is_dimensionless(&self) -> bool {
54 self.equals(&Self::dimensionless())
55 }
56}
57
58pub struct DimensionalAnalyzer {
60 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 fn initialize_si_units(&mut self) {
75 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 self.register_unit(
128 "N",
129 Dimensions {
130 length: 1,
131 mass: 1,
132 time: -2,
133 ..Dimensions::dimensionless()
134 },
135 ); self.register_unit(
137 "Pa",
138 Dimensions {
139 length: -1,
140 mass: 1,
141 time: -2,
142 ..Dimensions::dimensionless()
143 },
144 ); self.register_unit(
146 "J",
147 Dimensions {
148 length: 2,
149 mass: 1,
150 time: -2,
151 ..Dimensions::dimensionless()
152 },
153 ); self.register_unit(
155 "W",
156 Dimensions {
157 length: 2,
158 mass: 1,
159 time: -3,
160 ..Dimensions::dimensionless()
161 },
162 ); self.register_unit(
166 "V",
167 Dimensions {
168 length: 2,
169 mass: 1,
170 time: -3,
171 current: -1,
172 ..Dimensions::dimensionless()
173 },
174 ); self.register_unit(
176 "Ω",
177 Dimensions {
178 length: 2,
179 mass: 1,
180 time: -3,
181 current: -2,
182 ..Dimensions::dimensionless()
183 },
184 ); self.register_unit(
186 "C",
187 Dimensions {
188 time: 1,
189 current: 1,
190 ..Dimensions::dimensionless()
191 },
192 ); 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 ); self.register_unit(
213 "J/(kg*K)",
214 Dimensions {
215 length: 2,
216 time: -2,
217 temperature: -1,
218 ..Dimensions::dimensionless()
219 },
220 ); self.register_unit(
222 "kg/m^3",
223 Dimensions {
224 length: -3,
225 mass: 1,
226 ..Dimensions::dimensionless()
227 },
228 ); self.register_unit("dimensionless", Dimensions::dimensionless());
232 self.register_unit("1", Dimensions::dimensionless());
233 }
234
235 pub fn register_unit(&mut self, unit: &str, dimensions: Dimensions) {
237 self.unit_dimensions.insert(unit.to_string(), dimensions);
238 }
239
240 pub fn get_dimensions(&self, unit: &str) -> Option<&Dimensions> {
242 self.unit_dimensions.get(unit)
243 }
244
245 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 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
274pub struct UnitChecker {
276 analyzer: DimensionalAnalyzer,
277}
278
279impl UnitChecker {
280 pub fn new() -> Self {
281 Self {
282 analyzer: DimensionalAnalyzer::new(),
283 }
284 }
285
286 pub fn check_compatibility(&self, unit1: &str, unit2: &str) -> PhysicsResult<bool> {
288 self.analyzer.check_consistency(unit1, unit2)
289 }
290
291 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 pub fn canonical_form(&self, unit: &str) -> PhysicsResult<String> {
304 self.validate_unit(unit)?;
306
307 Ok(unit.to_string())
310 }
311
312 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 assert!(analyzer.check_consistency("m", "m").unwrap());
334 assert!(analyzer.check_consistency("kg", "kg").unwrap());
335
336 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 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 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 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 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 assert!(checker.validate_unit("invalid_unit").is_err());
388
389 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 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 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}