oxiphysics_core/dimensionless.rs
1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Dimensionless number calculations and similarity analysis.
5//!
6//! Provides standard dimensionless groups used in fluid mechanics, heat
7//! transfer, and related disciplines. All functions operate on SI-unit
8//! inputs and return the corresponding dimensionless ratio.
9
10// ---------------------------------------------------------------------------
11// Fluid mechanics
12// ---------------------------------------------------------------------------
13
14/// Reynolds number: Re = ρ·v·L / μ.
15///
16/// Characterises the ratio of inertial to viscous forces.
17///
18/// - `rho` — fluid density \[kg/m³\].
19/// - `v` — characteristic velocity \[m/s\].
20/// - `l` — characteristic length \[m\].
21/// - `mu` — dynamic viscosity \[Pa·s\].
22#[allow(dead_code)]
23pub fn reynolds_number(rho: f64, v: f64, l: f64, mu: f64) -> f64 {
24 rho * v * l / mu
25}
26
27/// Mach number: Ma = v / c_sound.
28///
29/// Ratio of flow velocity to the local speed of sound.
30///
31/// - `v` — flow speed \[m/s\].
32/// - `c_sound` — speed of sound in the medium \[m/s\].
33#[allow(dead_code)]
34pub fn mach_number(v: f64, c_sound: f64) -> f64 {
35 v / c_sound
36}
37
38/// Froude number: Fr = v / √(g·L).
39///
40/// Ratio of inertial to gravitational forces in free-surface flows.
41///
42/// - `v` — characteristic velocity \[m/s\].
43/// - `g` — gravitational acceleration \[m/s²\].
44/// - `l` — characteristic length \[m\].
45#[allow(dead_code)]
46pub fn froude_number(v: f64, g: f64, l: f64) -> f64 {
47 v / (g * l).sqrt()
48}
49
50/// Strouhal number: St = f·L / v.
51///
52/// Describes oscillating flow mechanisms.
53///
54/// - `f` — frequency of oscillation \[Hz\].
55/// - `l` — characteristic length \[m\].
56/// - `v` — characteristic velocity \[m/s\].
57#[allow(dead_code)]
58pub fn strouhal_number(f: f64, l: f64, v: f64) -> f64 {
59 f * l / v
60}
61
62/// Weber number: We = ρ·v²·L / σ.
63///
64/// Ratio of inertial force to surface tension.
65///
66/// - `rho` — fluid density \[kg/m³\].
67/// - `v` — characteristic velocity \[m/s\].
68/// - `l` — characteristic length \[m\].
69/// - `sigma` — surface tension \[N/m\].
70#[allow(dead_code)]
71pub fn weber_number(rho: f64, v: f64, l: f64, sigma: f64) -> f64 {
72 rho * v * v * l / sigma
73}
74
75/// Knudsen number: Kn = λ / L.
76///
77/// Ratio of the molecular mean free path to the characteristic length.
78/// Determines when continuum mechanics applies (Kn ≪ 1).
79///
80/// - `lambda_mfp` — mean free path \[m\].
81/// - `l` — characteristic length \[m\].
82#[allow(dead_code)]
83pub fn knudsen_number(lambda_mfp: f64, l: f64) -> f64 {
84 lambda_mfp / l
85}
86
87// ---------------------------------------------------------------------------
88// Heat and mass transfer
89// ---------------------------------------------------------------------------
90
91/// Nusselt number: Nu = h·L / k_thermal.
92///
93/// Ratio of convective to conductive heat transfer.
94///
95/// - `h` — convective heat transfer coefficient \[W/(m²·K)\].
96/// - `l` — characteristic length \[m\].
97/// - `k_thermal` — thermal conductivity \[W/(m·K)\].
98#[allow(dead_code)]
99pub fn nusselt_number(h: f64, l: f64, k_thermal: f64) -> f64 {
100 h * l / k_thermal
101}
102
103/// Prandtl number: Pr = μ·cp / k.
104///
105/// Ratio of momentum diffusivity to thermal diffusivity.
106///
107/// - `mu` — dynamic viscosity \[Pa·s\].
108/// - `cp` — specific heat capacity at constant pressure \[J/(kg·K)\].
109/// - `k` — thermal conductivity \[W/(m·K)\].
110#[allow(dead_code)]
111pub fn prandtl_number(mu: f64, cp: f64, k: f64) -> f64 {
112 mu * cp / k
113}
114
115/// Grashof number: Gr = g·β·ΔT·L³ / ν².
116///
117/// Ratio of buoyancy to viscous forces in natural convection.
118///
119/// - `g` — gravitational acceleration \[m/s²\].
120/// - `beta` — volumetric thermal expansion coefficient \[1/K\].
121/// - `delta_t` — temperature difference \[K\].
122/// - `l` — characteristic length \[m\].
123/// - `nu` — kinematic viscosity \[m²/s\].
124#[allow(dead_code)]
125pub fn grashof_number(g: f64, beta: f64, delta_t: f64, l: f64, nu: f64) -> f64 {
126 g * beta * delta_t * l * l * l / (nu * nu)
127}
128
129/// Péclet number: Pe = v·L / D.
130///
131/// Ratio of advective transport rate to diffusive transport rate.
132///
133/// - `v` — characteristic velocity \[m/s\].
134/// - `l` — characteristic length \[m\].
135/// - `d` — diffusion coefficient \[m²/s\].
136#[allow(dead_code)]
137pub fn peclet_number(v: f64, l: f64, d: f64) -> f64 {
138 v * l / d
139}
140
141// ---------------------------------------------------------------------------
142// Derived / combined numbers
143// ---------------------------------------------------------------------------
144
145/// Rayleigh number: Ra = Gr·Pr.
146///
147/// Used in natural convection; equals the product of the Grashof and Prandtl
148/// numbers.
149#[allow(dead_code)]
150pub fn rayleigh_number(g: f64, beta: f64, delta_t: f64, l: f64, nu: f64, alpha: f64) -> f64 {
151 g * beta * delta_t * l * l * l / (nu * alpha)
152}
153
154/// Biot number: Bi = h·L / k_s.
155///
156/// Ratio of internal thermal resistance to surface thermal resistance.
157///
158/// - `h` — convective heat transfer coefficient \[W/(m²·K)\].
159/// - `l` — characteristic length \[m\].
160/// - `k_s` — solid thermal conductivity \[W/(m·K)\].
161#[allow(dead_code)]
162pub fn biot_number(h: f64, l: f64, k_s: f64) -> f64 {
163 h * l / k_s
164}
165
166/// Euler number: Eu = ΔP / (ρ·v²).
167///
168/// Ratio of pressure forces to inertial forces.
169///
170/// - `delta_p` — pressure drop \[Pa\].
171/// - `rho` — fluid density \[kg/m³\].
172/// - `v` — characteristic velocity \[m/s\].
173#[allow(dead_code)]
174pub fn euler_number(delta_p: f64, rho: f64, v: f64) -> f64 {
175 delta_p / (rho * v * v)
176}
177
178/// Stokes number: Stk = t_p·v / l.
179///
180/// Characterises particle-flow coupling; high Stk means particles do not
181/// follow streamlines.
182///
183/// - `t_p` — particle relaxation time \[s\].
184/// - `v` — characteristic velocity \[m/s\].
185/// - `l` — characteristic length \[m\].
186#[allow(dead_code)]
187pub fn stokes_number(t_p: f64, v: f64, l: f64) -> f64 {
188 t_p * v / l
189}
190
191/// Archimedes number: Ar = g·L³·ρ_f·(ρ_p - ρ_f) / μ².
192///
193/// Determines the motion of fluids due to density differences.
194///
195/// - `g` — gravitational acceleration \[m/s²\].
196/// - `l` — characteristic length \[m\].
197/// - `rho_f` — fluid density \[kg/m³\].
198/// - `rho_p` — particle/second-phase density \[kg/m³\].
199/// - `mu` — dynamic viscosity \[Pa·s\].
200#[allow(dead_code)]
201pub fn archimedes_number(g: f64, l: f64, rho_f: f64, rho_p: f64, mu: f64) -> f64 {
202 g * l * l * l * rho_f * (rho_p - rho_f) / (mu * mu)
203}
204
205// ---------------------------------------------------------------------------
206// Tests
207// ---------------------------------------------------------------------------
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 // Water at ~20 °C
214 const RHO_WATER: f64 = 1000.0; // kg/m³
215 const MU_WATER: f64 = 1e-3; // Pa·s
216 const NU_WATER: f64 = 1e-6; // m²/s
217
218 // Air at ~20 °C
219 const C_SOUND_AIR: f64 = 343.0; // m/s
220 const G: f64 = 9.81; // m/s²
221
222 #[test]
223 fn test_reynolds_pipe_flow() {
224 // Pipe: D=0.01 m, v=1 m/s, water → Re = 1000*1*0.01/1e-3 = 10 000
225 let re = reynolds_number(RHO_WATER, 1.0, 0.01, MU_WATER);
226 assert!((re - 10_000.0).abs() < 1.0);
227 }
228
229 #[test]
230 fn test_reynolds_proportional_to_velocity() {
231 let re1 = reynolds_number(1.0, 1.0, 1.0, 1.0);
232 let re2 = reynolds_number(1.0, 2.0, 1.0, 1.0);
233 assert!((re2 - 2.0 * re1).abs() < 1e-12);
234 }
235
236 #[test]
237 fn test_mach_subsonic() {
238 let ma = mach_number(100.0, C_SOUND_AIR);
239 assert!(ma < 1.0, "100 m/s in air is subsonic");
240 assert!((ma - 100.0 / 343.0).abs() < 1e-10);
241 }
242
243 #[test]
244 fn test_mach_sonic() {
245 let ma = mach_number(C_SOUND_AIR, C_SOUND_AIR);
246 assert!((ma - 1.0).abs() < 1e-12);
247 }
248
249 #[test]
250 fn test_mach_supersonic() {
251 let ma = mach_number(2.0 * C_SOUND_AIR, C_SOUND_AIR);
252 assert!((ma - 2.0).abs() < 1e-12);
253 }
254
255 #[test]
256 fn test_froude_number_open_channel() {
257 // v=3 m/s, g=9.81, L=1 m → Fr = 3/√9.81 ≈ 0.9579
258 let fr = froude_number(3.0, G, 1.0);
259 let expected = 3.0 / G.sqrt();
260 assert!((fr - expected).abs() < 1e-10);
261 }
262
263 #[test]
264 fn test_froude_critical_flow() {
265 // Critical flow: Fr = 1 when v = √(g·L)
266 let l = 2.0;
267 let v = (G * l).sqrt();
268 let fr = froude_number(v, G, l);
269 assert!((fr - 1.0).abs() < 1e-10);
270 }
271
272 #[test]
273 fn test_strouhal_cylinder() {
274 // For a cylinder: St ≈ 0.2 at moderate Re
275 // f=2 Hz, L=0.1 m, v=1 m/s → St = 0.2
276 let st = strouhal_number(2.0, 0.1, 1.0);
277 assert!((st - 0.2).abs() < 1e-12);
278 }
279
280 #[test]
281 fn test_nusselt_number_basic() {
282 // h=100 W/(m²K), L=0.5 m, k=0.6 W/(mK) → Nu = 100*0.5/0.6 ≈ 83.33
283 let nu = nusselt_number(100.0, 0.5, 0.6);
284 assert!((nu - 100.0 * 0.5 / 0.6).abs() < 1e-10);
285 }
286
287 #[test]
288 fn test_prandtl_water() {
289 // Water ~20°C: μ=1e-3, cp=4182, k=0.598 → Pr ≈ 7.0
290 let pr = prandtl_number(MU_WATER, 4182.0, 0.598);
291 assert!(
292 pr > 6.0 && pr < 8.0,
293 "Prandtl for water should be ~7, got {pr}"
294 );
295 }
296
297 #[test]
298 fn test_prandtl_air() {
299 // Air ~20°C: μ=1.81e-5, cp=1005, k=0.0257 → Pr ≈ 0.71
300 let pr = prandtl_number(1.81e-5, 1005.0, 0.0257);
301 assert!(
302 pr > 0.6 && pr < 0.8,
303 "Prandtl for air should be ~0.71, got {pr}"
304 );
305 }
306
307 #[test]
308 fn test_grashof_number_positive_delta_t() {
309 // g=9.81, β=3.4e-3 K⁻¹ (water), ΔT=10 K, L=0.1 m, ν=1e-6 m²/s
310 let gr = grashof_number(G, 3.4e-3, 10.0, 0.1, NU_WATER);
311 assert!(gr > 0.0);
312 }
313
314 #[test]
315 fn test_grashof_scales_with_l_cubed() {
316 let gr1 = grashof_number(G, 1e-3, 10.0, 1.0, 1e-6);
317 let gr2 = grashof_number(G, 1e-3, 10.0, 2.0, 1e-6);
318 assert!((gr2 - 8.0 * gr1).abs() < 1e-6 * gr1.abs());
319 }
320
321 #[test]
322 fn test_weber_number_basic() {
323 // ρ=1000, v=1, L=0.01, σ=0.072 (water-air) → We = 1000*1*0.01/0.072 ≈ 138.9
324 let we = weber_number(1000.0, 1.0, 0.01, 0.072);
325 assert!((we - 1000.0 * 0.01 / 0.072).abs() < 0.1);
326 }
327
328 #[test]
329 fn test_knudsen_continuum_limit() {
330 // λ_mfp = 70 nm, L = 1 m → Kn ~ 7e-8 ≪ 1 (continuum)
331 let kn = knudsen_number(70e-9, 1.0);
332 assert!(kn < 1e-6, "Kn should be tiny in continuum regime");
333 }
334
335 #[test]
336 fn test_knudsen_free_molecular() {
337 // λ_mfp ~ L → Kn ~ 1 (free molecular regime)
338 let kn = knudsen_number(1.0, 1.0);
339 assert!((kn - 1.0).abs() < 1e-12);
340 }
341
342 #[test]
343 fn test_peclet_number_basic() {
344 // v=1, L=1, D=1e-3 → Pe = 1000
345 let pe = peclet_number(1.0, 1.0, 1e-3);
346 assert!((pe - 1000.0).abs() < 1e-10);
347 }
348
349 #[test]
350 fn test_peclet_proportional_to_velocity() {
351 let pe1 = peclet_number(1.0, 1.0, 1.0);
352 let pe2 = peclet_number(3.0, 1.0, 1.0);
353 assert!((pe2 - 3.0 * pe1).abs() < 1e-12);
354 }
355
356 #[test]
357 fn test_rayleigh_number_positive() {
358 // α ≈ 1.4e-7 m²/s (water thermal diffusivity)
359 let ra = rayleigh_number(G, 3.4e-3, 10.0, 0.1, NU_WATER, 1.4e-7);
360 assert!(ra > 0.0);
361 }
362
363 #[test]
364 fn test_biot_number_small_lump() {
365 // Bi < 0.1 → lumped capacitance valid
366 let bi = biot_number(10.0, 0.005, 200.0); // aluminium
367 assert!(bi < 0.1, "Bi = {bi} should be < 0.1");
368 }
369
370 #[test]
371 fn test_biot_number_large() {
372 let bi = biot_number(1000.0, 0.1, 1.0);
373 assert!(bi > 1.0);
374 }
375
376 #[test]
377 fn test_euler_number_definition() {
378 // ΔP = 100 Pa, ρ=1, v=10 → Eu = 100/(1*100) = 1
379 let eu = euler_number(100.0, 1.0, 10.0);
380 assert!((eu - 1.0).abs() < 1e-12);
381 }
382
383 #[test]
384 fn test_stokes_number_unity() {
385 // t_p=1, v=1, L=1 → Stk=1
386 let stk = stokes_number(1.0, 1.0, 1.0);
387 assert!((stk - 1.0).abs() < 1e-12);
388 }
389
390 #[test]
391 fn test_stokes_number_small() {
392 // Small particle: t_p very small → Stk ≪ 1
393 let stk = stokes_number(1e-6, 1.0, 0.1);
394 assert!(stk < 1e-4);
395 }
396
397 #[test]
398 fn test_archimedes_number_positive_buoyancy() {
399 // ρ_p > ρ_f → Ar > 0
400 let ar = archimedes_number(G, 0.01, 1000.0, 2500.0, MU_WATER);
401 assert!(ar > 0.0);
402 }
403
404 #[test]
405 fn test_archimedes_number_negative_buoyancy() {
406 // ρ_p < ρ_f → Ar < 0 (particle floats)
407 let ar = archimedes_number(G, 0.01, 1000.0, 500.0, MU_WATER);
408 assert!(ar < 0.0);
409 }
410
411 #[test]
412 fn test_all_dimensionless_finite() {
413 // Quick sanity: no NaN or infinity for positive inputs
414 assert!(reynolds_number(1.0, 1.0, 1.0, 1.0).is_finite());
415 assert!(mach_number(1.0, 1.0).is_finite());
416 assert!(froude_number(1.0, 1.0, 1.0).is_finite());
417 assert!(strouhal_number(1.0, 1.0, 1.0).is_finite());
418 assert!(nusselt_number(1.0, 1.0, 1.0).is_finite());
419 assert!(prandtl_number(1.0, 1.0, 1.0).is_finite());
420 assert!(grashof_number(1.0, 1.0, 1.0, 1.0, 1.0).is_finite());
421 assert!(weber_number(1.0, 1.0, 1.0, 1.0).is_finite());
422 assert!(knudsen_number(1.0, 1.0).is_finite());
423 assert!(peclet_number(1.0, 1.0, 1.0).is_finite());
424 }
425
426 #[test]
427 fn test_reynolds_turbulent_threshold() {
428 // Pipe flow transitions to turbulence at Re ≈ 4000
429 let re_laminar = reynolds_number(1000.0, 0.1, 0.01, 1e-3); // Re=1000
430 let re_turbulent = reynolds_number(1000.0, 1.0, 0.01, 1e-3); // Re=10000
431 assert!(re_laminar < 4000.0);
432 assert!(re_turbulent > 4000.0);
433 }
434}