use std::f64::consts::PI;
use crate::units::conversion::BOLTZMANN;
const BETA_1_L: f64 = 1.875_104_069;
const C: f64 = 299_792_458.0;
#[derive(Debug, Clone, PartialEq)]
pub enum CantileverMaterial {
Silicon,
SiliconNitride,
Gold,
Custom {
youngs_modulus: f64,
density: f64,
},
}
impl CantileverMaterial {
pub fn youngs_modulus(&self) -> f64 {
match self {
CantileverMaterial::Silicon => 130.0e9,
CantileverMaterial::SiliconNitride => 250.0e9,
CantileverMaterial::Gold => 79.0e9,
CantileverMaterial::Custom { youngs_modulus, .. } => *youngs_modulus,
}
}
pub fn density(&self) -> f64 {
match self {
CantileverMaterial::Silicon => 2_330.0,
CantileverMaterial::SiliconNitride => 3_100.0,
CantileverMaterial::Gold => 19_300.0,
CantileverMaterial::Custom { density, .. } => *density,
}
}
}
#[derive(Debug, Clone)]
pub struct OpticalCantilever {
pub length: f64,
pub width: f64,
pub thickness: f64,
pub youngs_modulus: f64,
pub density: f64,
pub q_factor: f64,
}
impl OpticalCantilever {
pub fn new(length: f64, width: f64, thickness: f64, material: CantileverMaterial) -> Self {
Self {
length,
width,
thickness,
youngs_modulus: material.youngs_modulus(),
density: material.density(),
q_factor: 1_000.0,
}
}
pub fn moment_of_inertia(&self) -> f64 {
self.width * self.thickness.powi(3) / 12.0
}
fn cross_section_area(&self) -> f64 {
self.width * self.thickness
}
pub fn effective_mass(&self) -> f64 {
0.2427 * self.density * self.cross_section_area() * self.length
}
pub fn resonant_frequency(&self) -> f64 {
let ei = self.youngs_modulus * self.moment_of_inertia();
let rho_a = self.density * self.cross_section_area();
let l2 = self.length * self.length;
BETA_1_L * BETA_1_L / (2.0 * PI * l2) * (ei / rho_a).sqrt()
}
pub fn spring_constant(&self) -> f64 {
3.0 * self.youngs_modulus * self.moment_of_inertia() / self.length.powi(3)
}
pub fn deflection_from_force(&self, force: f64) -> f64 {
force / self.spring_constant()
}
pub fn optical_force_deflection(&self, power_w: f64, reflectivity: f64) -> f64 {
let r = reflectivity.clamp(0.0, 1.0);
let force = power_w * (1.0 + r) / C;
self.deflection_from_force(force)
}
pub fn thermo_mechanical_noise(&self, temperature_k: f64) -> f64 {
let omega0 = 2.0 * PI * self.resonant_frequency();
let k = self.spring_constant();
(4.0 * BOLTZMANN * temperature_k * k / (omega0 * self.q_factor)).sqrt()
}
pub fn mass_sensitivity(&self) -> f64 {
let omega0 = 2.0 * PI * self.resonant_frequency();
omega0 / (2.0 * self.effective_mass())
}
pub fn minimum_detectable_mass(&self, df_hz: f64) -> f64 {
2.0 * PI * df_hz / self.mass_sensitivity()
}
pub fn optical_lever_sensitivity(&self, lever_arm_m: f64) -> f64 {
2.0 * lever_arm_m / self.length
}
pub fn frequency_response(&self, f_drive: f64) -> f64 {
let f0 = self.resonant_frequency();
let ratio = f_drive / f0;
let denom = ((1.0 - ratio * ratio).powi(2) + (ratio / self.q_factor).powi(2)).sqrt();
if denom < f64::EPSILON {
self.q_factor
} else {
1.0 / denom
}
}
pub fn bandwidth_3db(&self) -> f64 {
self.resonant_frequency() / self.q_factor
}
pub fn deflection_profile(&self, x: f64, force: f64) -> f64 {
let x_clamped = x.clamp(0.0, self.length);
let ei = self.youngs_modulus * self.moment_of_inertia();
force * x_clamped * x_clamped * (3.0 * self.length - x_clamped) / (6.0 * ei)
}
pub fn bending_stress(&self, x: f64, force: f64) -> f64 {
let x_clamped = x.clamp(0.0, self.length);
let ei = self.youngs_modulus * self.moment_of_inertia();
let y_max = self.thickness / 2.0;
self.youngs_modulus * y_max * force * (self.length - x_clamped) / ei
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn silicon_cantilever() -> OpticalCantilever {
OpticalCantilever::new(100e-6, 10e-6, 1e-6, CantileverMaterial::Silicon)
}
#[test]
fn test_spring_constant_silicon() {
let c = silicon_cantilever();
let k = c.spring_constant();
assert!(
k > 0.1 && k < 1.0,
"spring constant should be ~0.325 N/m, got {k}"
);
let i = c.moment_of_inertia();
let k_manual = 3.0 * 130e9 * i / (100e-6f64).powi(3);
assert_abs_diff_eq!(k, k_manual, epsilon = 1e-10);
}
#[test]
fn test_resonant_frequency_range() {
let c = silicon_cantilever();
let f0 = c.resonant_frequency();
assert!(
f0 > 10_000.0 && f0 < 1_000_000.0,
"resonant frequency {f0} Hz out of expected range"
);
}
#[test]
fn test_deflection_from_force() {
let c = silicon_cantilever();
let force = 1e-9; let delta = c.deflection_from_force(force);
assert!(
delta > 1e-10 && delta < 1e-7,
"deflection {delta} m out of expected range"
);
let k = c.spring_constant();
assert_abs_diff_eq!(delta * k, force, epsilon = 1e-20);
}
#[test]
fn test_thermo_mechanical_noise_positive() {
let c = silicon_cantilever();
let noise = c.thermo_mechanical_noise(300.0); assert!(noise > 0.0, "noise should be positive");
assert!(noise < 1e-9, "noise should be sub-nm/√Hz at room temp");
}
#[test]
fn test_optical_force_deflection() {
let c = silicon_cantilever();
let delta = c.optical_force_deflection(1e-3, 1.0);
assert!(delta > 0.0, "optical force deflection must be positive");
assert!(delta < 1e-9, "optical deflection should be sub-nm for 1 mW");
}
#[test]
fn test_mass_sensitivity() {
let c = silicon_cantilever();
let sens = c.mass_sensitivity(); assert!(sens > 0.0, "mass sensitivity should be positive");
assert!(
sens > 1e10,
"mass sensitivity should be large for microscale cantilever"
);
}
#[test]
fn test_gold_vs_silicon_q_density() {
let si = OpticalCantilever::new(100e-6, 10e-6, 1e-6, CantileverMaterial::Silicon);
let au = OpticalCantilever::new(100e-6, 10e-6, 1e-6, CantileverMaterial::Gold);
assert!(
au.effective_mass() > si.effective_mass(),
"gold cantilever should have greater effective mass"
);
assert!(
au.resonant_frequency() < si.resonant_frequency(),
"gold cantilever should have lower resonant frequency"
);
}
#[test]
fn test_deflection_profile_at_tip() {
let c = silicon_cantilever();
let force = 1e-9;
let tip_profile = c.deflection_profile(c.length, force);
let tip_direct = c.deflection_from_force(force);
assert_abs_diff_eq!(tip_profile, tip_direct, epsilon = 1e-18);
}
}