Skip to main content

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}