use std::f64::consts::PI;
const C: f64 = 299_792_458.0;
const EPSILON_0: f64 = 8.854_187_812_8e-12;
#[derive(Debug, Clone, PartialEq)]
pub enum FpMemsError {
PullIn {
requested: f64,
pull_in: f64,
},
OutOfRange {
requested: f64,
min: f64,
max: f64,
},
ConvergenceFailure,
}
impl std::fmt::Display for FpMemsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FpMemsError::PullIn { requested, pull_in } => write!(
f,
"pull-in instability: requested {requested:.3} V exceeds pull-in voltage {pull_in:.3} V"
),
FpMemsError::OutOfRange { requested, min, max } => write!(
f,
"cavity length {requested:.3e} m is outside achievable range [{min:.3e}, {max:.3e}] m"
),
FpMemsError::ConvergenceFailure => write!(f, "electrostatic solver failed to converge"),
}
}
}
impl std::error::Error for FpMemsError {}
#[derive(Debug, Clone)]
pub struct MemosFabryPerot {
pub cavity_length: f64,
pub min_length: f64,
pub max_length: f64,
pub r1: f64,
pub r2: f64,
pub medium_index: f64,
pub finesse: f64,
pub spring_constant: f64,
pub actuation_voltage: f64,
pub pull_in_voltage: f64,
electrode_area: f64,
d0: f64,
}
impl MemosFabryPerot {
pub fn new(d0: f64, r1: f64, r2: f64, k: f64, area: f64) -> Self {
let r1 = r1.clamp(0.0, 1.0);
let r2 = r2.clamp(0.0, 1.0);
let finesse = Self::compute_finesse(r1, r2);
let pull_in = Self::compute_pull_in_voltage(k, d0, area);
Self {
cavity_length: d0,
min_length: 2.0 * d0 / 3.0,
max_length: d0,
r1,
r2,
medium_index: 1.0,
finesse,
spring_constant: k,
actuation_voltage: 0.0,
pull_in_voltage: pull_in,
electrode_area: area,
d0,
}
}
pub fn finesse(&self) -> f64 {
self.finesse
}
fn compute_finesse(r1: f64, r2: f64) -> f64 {
let r_eff = (r1 * r2).sqrt();
if (1.0 - r_eff).abs() < f64::EPSILON {
f64::INFINITY
} else {
PI * (r1 * r2).powf(0.25) / (1.0 - r_eff)
}
}
pub fn fsr(&self) -> f64 {
C / (2.0 * self.medium_index * self.cavity_length)
}
pub fn linewidth(&self) -> f64 {
self.fsr() / self.finesse
}
pub fn resonant_wavelength(&self, order: usize) -> Option<f64> {
if order == 0 {
return None;
}
Some(2.0 * self.medium_index * self.cavity_length / order as f64)
}
fn compute_pull_in_voltage(k: f64, d0: f64, area: f64) -> f64 {
(8.0 * k * d0.powi(3) / (27.0 * EPSILON_0 * area)).sqrt()
}
pub fn pull_in_voltage(&self) -> f64 {
self.pull_in_voltage
}
fn solve_gap_newton(&self, voltage: f64) -> Result<f64, FpMemsError> {
let vv = EPSILON_0 * self.electrode_area * voltage * voltage / 2.0;
let mut d = self.d0; for _ in 0..500 {
let fd = self.spring_constant * (self.d0 - d) * d * d - vv;
let fpd = self.spring_constant * d * (2.0 * self.d0 - 3.0 * d);
if fpd.abs() < 1e-50 {
return Err(FpMemsError::ConvergenceFailure);
}
let d_next = d - fd / fpd;
let d_next = d_next.max(2.0 * self.d0 / 3.0 + 1e-15);
if (d_next - d).abs() < 1e-18 {
return Ok(d_next);
}
d = d_next;
}
Err(FpMemsError::ConvergenceFailure)
}
pub fn tune_voltage(&mut self, voltage: f64) -> Result<f64, FpMemsError> {
let vpi = self.pull_in_voltage;
if voltage >= vpi {
return Err(FpMemsError::PullIn {
requested: voltage,
pull_in: vpi,
});
}
let new_d = self.solve_gap_newton(voltage)?;
self.cavity_length = new_d;
self.actuation_voltage = voltage;
Ok(new_d)
}
pub fn reset(&mut self) {
self.cavity_length = self.d0;
self.actuation_voltage = 0.0;
}
pub fn transmission(&self, wavelength: f64) -> f64 {
let r = (self.r1 * self.r2).sqrt();
let phase = 4.0 * PI * self.medium_index * self.cavity_length / wavelength;
let f_coeff = 4.0 * r / (1.0 - r).powi(2);
let t_max = (1.0 - self.r1) * (1.0 - self.r2) / (1.0 - r).powi(2);
t_max / (1.0 + f_coeff * (phase / 2.0).sin().powi(2))
}
pub fn tuning_range(&self, order: usize) -> Option<(f64, f64)> {
if order == 0 {
return None;
}
let lambda_max = 2.0 * self.medium_index * self.d0 / order as f64;
let lambda_min = 2.0 * self.medium_index * self.min_length / order as f64;
Some((lambda_min, lambda_max))
}
pub fn electrostatic_force(&self) -> f64 {
if self.actuation_voltage == 0.0 {
return 0.0;
}
EPSILON_0 * self.electrode_area * self.actuation_voltage.powi(2)
/ (2.0 * self.cavity_length.powi(2))
}
pub fn restoring_force(&self) -> f64 {
self.spring_constant * (self.d0 - self.cavity_length)
}
pub fn wavelength_sensitivity(&self, order: usize) -> Option<f64> {
if order == 0 {
return None;
}
let dv = self.pull_in_voltage * 1e-5;
let v0 = self.actuation_voltage;
let mut fp_lo = self.clone();
let mut fp_hi = self.clone();
let v_lo = (v0 - dv).max(0.0);
let v_hi = v0 + dv;
if v_hi >= self.pull_in_voltage {
return None;
}
if fp_lo.tune_voltage(v_lo).is_err() || fp_hi.tune_voltage(v_hi).is_err() {
return None;
}
let lambda_lo = 2.0 * self.medium_index * fp_lo.cavity_length / order as f64;
let lambda_hi = 2.0 * self.medium_index * fp_hi.cavity_length / order as f64;
Some((lambda_hi - lambda_lo) / (v_hi - v_lo))
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn make_fp() -> MemosFabryPerot {
MemosFabryPerot::new(5e-6, 0.95, 0.95, 50.0, 100e-12)
}
#[test]
fn test_finesse() {
let fp = make_fp();
let f = fp.finesse();
assert!(f > 50.0 && f < 80.0, "finesse {f} out of expected range");
}
#[test]
fn test_fsr() {
let fp = make_fp();
let fsr = fp.fsr();
assert_abs_diff_eq!(fsr, C / (2.0 * 5e-6), epsilon = 1e6);
}
#[test]
fn test_resonant_wavelength() {
let fp = make_fp();
let lambda = fp.resonant_wavelength(10).expect("order 10 should work");
assert_abs_diff_eq!(lambda, 1.0e-6, epsilon = 1e-12);
}
#[test]
fn test_pull_in_voltage_positive() {
let fp = make_fp();
let vpi = fp.pull_in_voltage();
assert!(vpi > 0.0, "pull-in voltage must be positive");
}
#[test]
fn test_tune_below_pull_in() {
let mut fp = make_fp();
let vpi = fp.pull_in_voltage();
let v_safe = vpi * 0.5;
let result = fp.tune_voltage(v_safe);
assert!(result.is_ok(), "voltage below pull-in should succeed");
let new_d = result.expect("already checked ok");
assert!(
new_d < fp.d0,
"cavity should compress under positive voltage"
);
assert!(
new_d > fp.min_length - 1e-12,
"gap should not collapse below pull-in point"
);
}
#[test]
fn test_tune_at_pull_in_fails() {
let mut fp = make_fp();
let vpi = fp.pull_in_voltage();
let result = fp.tune_voltage(vpi);
assert!(
matches!(result, Err(FpMemsError::PullIn { .. })),
"voltage at pull-in should return PullIn error"
);
}
#[test]
fn test_transmission_at_resonance() {
let fp = make_fp();
let lambda = fp.resonant_wavelength(10).expect("valid order");
let t = fp.transmission(lambda);
assert!(t > 0.9, "transmission at resonance should be high, got {t}");
}
#[test]
fn test_transmission_off_resonance() {
let fp = make_fp();
let lambda = fp.resonant_wavelength(10).expect("valid order");
let lambda_off = lambda * (1.0 + 0.5 / fp.finesse());
let t_off = fp.transmission(lambda_off);
let t_on = fp.transmission(lambda);
assert!(t_off < t_on, "off-resonance transmission should be lower");
}
#[test]
fn test_tuning_range_consistent() {
let fp = make_fp();
let (lmin, lmax) = fp.tuning_range(10).expect("valid order");
assert!(lmin < lmax, "min wavelength must be less than max");
assert_abs_diff_eq!(lmax / lmin, fp.d0 / fp.min_length, epsilon = 1e-10);
}
#[test]
fn test_reset_restores_gap() {
let mut fp = make_fp();
let vpi = fp.pull_in_voltage();
fp.tune_voltage(vpi * 0.5).expect("tune should succeed");
fp.reset();
assert_abs_diff_eq!(fp.cavity_length, fp.d0, epsilon = 1e-18);
assert_abs_diff_eq!(fp.actuation_voltage, 0.0, epsilon = 1e-18);
}
}