const KB: f64 = 1.380649e-23; const PI: f64 = std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct PhotophoreticForce {
pub radius_m: f64,
pub absorption_coeff: f64,
pub gas_pressure_pa: f64,
pub gas_thermal_cond: f64,
pub particle_thermal_cond: f64,
pub gas_viscosity: f64,
pub temperature_k: f64,
}
impl PhotophoreticForce {
pub fn new(radius_m: f64, gas_pressure_pa: f64, temperature_k: f64) -> Self {
Self {
radius_m,
absorption_coeff: 0.5, gas_pressure_pa,
gas_thermal_cond: 0.026, particle_thermal_cond: 1.0, gas_viscosity: 1.81e-5, temperature_k,
}
}
pub fn new_full(
radius_m: f64,
absorption_coeff: f64,
gas_pressure_pa: f64,
gas_thermal_cond: f64,
particle_thermal_cond: f64,
gas_viscosity: f64,
temperature_k: f64,
) -> Self {
Self {
radius_m,
absorption_coeff,
gas_pressure_pa,
gas_thermal_cond,
particle_thermal_cond,
gas_viscosity,
temperature_k,
}
}
pub fn mean_free_path(&self) -> f64 {
let m_air = 4.81e-26; let numerator = self.gas_viscosity * (PI * KB * self.temperature_k / (2.0 * m_air)).sqrt();
if self.gas_pressure_pa <= 0.0 {
return f64::INFINITY;
}
numerator / self.gas_pressure_pa
}
pub fn knudsen_number(&self) -> f64 {
if self.radius_m <= 0.0 {
return f64::INFINITY;
}
self.mean_free_path() / self.radius_m
}
pub fn j1_asymmetry(&self) -> f64 {
if self.gas_thermal_cond <= 0.0 {
return 0.5;
}
let k_ratio = self.particle_thermal_cond / self.gas_thermal_cond; let c_t = 2.18; (k_ratio + c_t) / (k_ratio + 2.0 * c_t)
}
pub fn force(&self, intensity_w_m2: f64) -> f64 {
let lam = self.mean_free_path();
let r = self.radius_m;
let t0 = self.temperature_k;
let j1 = self.j1_asymmetry();
let q_abs = self.absorption_coeff;
let p = self.gas_pressure_pa;
let c_s = 1.17;
let alpha_ph = p * lam * r / (t0 * (r + c_s * lam));
j1 * q_abs * PI * r * r * intensity_w_m2 * alpha_ph / (4.0 * self.gas_thermal_cond * t0)
}
pub fn force_signed(&self, intensity_w_m2: f64) -> f64 {
self.force(intensity_w_m2)
}
pub fn photophoretic_pressure(&self) -> f64 {
if self.radius_m <= 0.0 {
return 0.0;
}
let lam = self.mean_free_path();
let r = self.radius_m;
let t0 = self.temperature_k;
let j1 = self.j1_asymmetry();
let q_abs = self.absorption_coeff;
let p = self.gas_pressure_pa;
let c_s = 1.17;
let alpha_ph = p * lam * r / (t0 * (r + c_s * lam));
j1 * q_abs * alpha_ph / (4.0 * self.gas_thermal_cond * t0)
}
pub fn levitation_intensity(&self, particle_density_kg_m3: f64) -> f64 {
let r = self.radius_m;
let g = 9.80665; let weight = (4.0 / 3.0) * PI * r * r * r * particle_density_kg_m3 * g;
let f_per_intensity = PI * r * r * self.photophoretic_pressure();
if f_per_intensity <= 0.0 {
return f64::INFINITY;
}
weight / f_per_intensity
}
}
pub fn thermophoretic_force(
radius_m: f64,
gas_viscosity: f64,
gas_thermal_cond: f64,
particle_thermal_cond: f64,
temperature_gradient: f64,
temperature_k: f64,
) -> f64 {
if temperature_k <= 0.0 || gas_thermal_cond <= 0.0 {
return 0.0;
}
let k_ratio = gas_thermal_cond / (gas_thermal_cond + particle_thermal_cond);
let c_s = 1.17;
let k_t = 2.0 * c_s * k_ratio;
6.0 * PI * gas_viscosity * radius_m * k_t * temperature_gradient / temperature_k
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mean_free_path_air_standard_conditions() {
let ph = PhotophoreticForce::new(1e-6, 101325.0, 300.0);
let lam = ph.mean_free_path();
assert!(
lam > 40e-9 && lam < 100e-9,
"Air mean free path at STP = {} nm, expected ~66 nm",
lam * 1e9
);
}
#[test]
fn knudsen_number_continuum_regime() {
let ph = PhotophoreticForce::new(10e-6, 101325.0, 300.0);
let kn = ph.knudsen_number();
assert!(kn < 0.1, "Kn={} for 10µm particle should be << 1", kn);
}
#[test]
fn knudsen_number_free_molecular_regime() {
let ph = PhotophoreticForce::new(10e-9, 101325.0, 300.0);
let kn = ph.knudsen_number();
assert!(kn > 1.0, "Kn={} for 10nm particle at STP should be > 1", kn);
}
#[test]
fn j1_asymmetry_bounds() {
let ph_low =
PhotophoreticForce::new_full(1e-6, 0.5, 101325.0, 0.026, 0.001, 1.81e-5, 300.0);
let ph_high =
PhotophoreticForce::new_full(1e-6, 0.5, 101325.0, 0.026, 100.0, 1.81e-5, 300.0);
let j1_low = ph_low.j1_asymmetry();
let j1_high = ph_high.j1_asymmetry();
assert!(
j1_low > 0.0 && j1_low <= 0.6,
"J₁(low κ_p) = {} should be near 0.5",
j1_low
);
assert!(
j1_high > 0.9 && j1_high < 1.0,
"J₁(high κ_p) = {} should approach 1",
j1_high
);
}
#[test]
fn photophoretic_force_positive_for_absorber() {
let ph = PhotophoreticForce::new(1e-6, 101325.0, 300.0);
let f = ph.force(1e6); assert!(
f > 0.0,
"Photophoretic force must be positive for absorbing particle, got {}",
f
);
}
#[test]
fn photophoretic_force_scales_with_intensity() {
let ph = PhotophoreticForce::new(1e-6, 101325.0, 300.0);
let f1 = ph.force(1e6);
let f2 = ph.force(2e6);
assert!(
(f2 / f1 - 2.0).abs() < 0.01,
"Force should scale linearly with intensity: f2/f1={}",
f2 / f1
);
}
#[test]
fn thermophoretic_force_positive() {
let f = thermophoretic_force(
1e-6, 1.81e-5, 0.026, 1.0, 1000.0, 300.0, );
assert!(f > 0.0, "Thermophoretic force must be positive, got {}", f);
assert!(f < 1e-10, "Thermophoretic force {} seems too large", f);
}
#[test]
fn levitation_intensity_order_of_magnitude() {
let ph = PhotophoreticForce::new_full(
1e-6, 0.8, 101325.0, 0.026, 0.2, 1.81e-5, 300.0,
);
let density_soot = 1700.0; let i_lev = ph.levitation_intensity(density_soot);
assert!(
i_lev > 1e3 && i_lev < 1e15,
"Levitation intensity {} W/m² seems unreasonable",
i_lev
);
}
}