use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct MemosTiltMirror {
pub mirror_size: f64,
pub resonant_freq_x: f64,
pub resonant_freq_y: f64,
pub q_factor_x: f64,
pub q_factor_y: f64,
pub max_angle_x: f64,
pub max_angle_y: f64,
pub drive_voltage: f64,
}
impl MemosTiltMirror {
pub fn new(size: f64, freq_x: f64, freq_y: f64, q_x: f64, q_y: f64) -> Self {
Self {
mirror_size: size,
resonant_freq_x: freq_x,
resonant_freq_y: freq_y,
q_factor_x: q_x,
q_factor_y: q_y,
max_angle_x: 0.2,
max_angle_y: 0.2,
drive_voltage: 100.0,
}
}
pub fn scan_lissajous(&self, t: f64, freq_ratio: f64) -> [f64; 2] {
let theta_x = self.max_angle_x * (2.0 * PI * self.resonant_freq_x * t).sin();
let theta_y =
self.max_angle_y * (2.0 * PI * self.resonant_freq_y * freq_ratio * t + PI / 2.0).sin();
[theta_x, theta_y]
}
pub fn scan_raster(&self, t: f64, line_time: f64, n_lines: usize) -> [f64; 2] {
if n_lines == 0 || line_time <= 0.0 {
return [0.0, 0.0];
}
let theta_x = self.max_angle_x * (2.0 * PI * self.resonant_freq_x * t).sin();
let frame_time = line_time * n_lines as f64;
let t_in_frame = t.rem_euclid(frame_time);
let line_idx = (t_in_frame / line_time) as usize;
let n = n_lines as f64;
let theta_y = self.max_angle_y * (2.0 * (line_idx as f64 / (n - 1.0).max(1.0)) - 1.0);
[theta_x, theta_y]
}
pub fn beam_deflection(&self, angle: f64, distance: f64) -> f64 {
2.0 * angle * distance
}
pub fn bandwidth_3db(&self) -> f64 {
self.resonant_freq_x / self.q_factor_x
}
pub fn actuation_sensitivity(&self) -> f64 {
self.max_angle_x / self.drive_voltage
}
pub fn frequency_response_x(&self, f: f64) -> f64 {
let ratio = f / self.resonant_freq_x;
let denom = ((1.0 - ratio * ratio).powi(2) + (ratio / self.q_factor_x).powi(2)).sqrt();
if denom < f64::EPSILON {
self.q_factor_x
} else {
1.0 / denom
}
}
pub fn frequency_response_y(&self, f: f64) -> f64 {
let ratio = f / self.resonant_freq_y;
let denom = ((1.0 - ratio * ratio).powi(2) + (ratio / self.q_factor_y).powi(2)).sqrt();
if denom < f64::EPSILON {
self.q_factor_y
} else {
1.0 / denom
}
}
pub fn field_of_view(&self) -> [f64; 2] {
[2.0 * self.max_angle_x, 2.0 * self.max_angle_y]
}
pub fn moment_of_inertia(&self) -> f64 {
let areal_density = 2_330.0 * 1e-6; let m = areal_density * self.mirror_size * self.mirror_size;
m * self.mirror_size * self.mirror_size / 6.0
}
}
#[derive(Debug, Clone)]
pub struct GimbalMirror {
pub inner_mirror: MemosTiltMirror,
pub outer_frame_freq: f64,
pub outer_frame_q: f64,
}
impl GimbalMirror {
pub fn new(inner: MemosTiltMirror, outer_freq: f64, outer_q: f64) -> Self {
Self {
inner_mirror: inner,
outer_frame_freq: outer_freq,
outer_frame_q: outer_q,
}
}
pub fn coupled_response(&self, drive_freq: f64, drive_amp: f64) -> [f64; 2] {
let h_inner = self.inner_mirror.frequency_response_x(drive_freq);
let ratio_outer = drive_freq / self.outer_frame_freq;
let h_outer = {
let denom = ((1.0 - ratio_outer * ratio_outer).powi(2)
+ (ratio_outer / self.outer_frame_q).powi(2))
.sqrt();
if denom < f64::EPSILON {
self.outer_frame_q
} else {
1.0 / denom
}
};
let theta_x = self.inner_mirror.actuation_sensitivity() * drive_amp * h_inner;
let theta_y = self.inner_mirror.actuation_sensitivity() * drive_amp * h_outer;
[theta_x, theta_y]
}
pub fn scan_lissajous(&self, t: f64) -> [f64; 2] {
let theta_x = self.inner_mirror.max_angle_x
* (2.0 * PI * self.inner_mirror.resonant_freq_x * t).sin();
let theta_y =
self.inner_mirror.max_angle_y * (2.0 * PI * self.outer_frame_freq * t + PI / 2.0).sin();
[theta_x, theta_y]
}
}
#[derive(Debug, Clone)]
pub struct MemosVoa {
pub gap: f64,
pub wavelength: f64,
pub coupling_loss_db: f64,
beam_waist: f64,
}
impl MemosVoa {
pub fn new(wavelength: f64) -> Self {
let w0_fiber = 5e-6; let gap = 200e-6;
let z_r = PI * w0_fiber * w0_fiber / wavelength;
let z = gap / 2.0;
let w_gap = w0_fiber * (1.0 + (z / z_r).powi(2)).sqrt();
Self {
gap,
wavelength,
coupling_loss_db: 0.5, beam_waist: w_gap,
}
}
pub fn attenuation_db(&self, displacement: f64) -> f64 {
let t = (-(displacement / self.beam_waist).powi(2)).exp();
if t <= 0.0 {
f64::INFINITY
} else {
-10.0 * t.log10()
}
}
pub fn insertion_loss_db(&self) -> f64 {
self.coupling_loss_db
}
pub fn total_loss_db(&self, displacement: f64) -> f64 {
self.insertion_loss_db() + self.attenuation_db(displacement)
}
pub fn displacement_for_attenuation(&self, att_db: f64) -> Option<f64> {
if att_db <= 0.0 {
return None;
}
let dx = self.beam_waist * (att_db * 10_f64.ln() / 10.0).sqrt();
Some(dx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn make_tilt_mirror() -> MemosTiltMirror {
MemosTiltMirror::new(1e-3, 1000.0, 500.0, 200.0, 150.0)
}
#[test]
fn test_lissajous_within_fov() {
let m = make_tilt_mirror();
for i in 0..100 {
let t = i as f64 * 1e-5;
let [tx, ty] = m.scan_lissajous(t, 1.5);
assert!(tx.abs() <= m.max_angle_x + 1e-12, "tx out of range: {tx}");
assert!(ty.abs() <= m.max_angle_y + 1e-12, "ty out of range: {ty}");
}
}
#[test]
fn test_raster_x_sinusoidal() {
let m = make_tilt_mirror();
let line_time = 1e-3;
let n_lines = 100;
let [tx0, _] = m.scan_raster(0.0, line_time, n_lines);
assert_abs_diff_eq!(tx0, 0.0, epsilon = 1e-12);
let t_max = 1.0 / (4.0 * m.resonant_freq_x);
let [tx_max, _] = m.scan_raster(t_max, line_time, n_lines);
assert_abs_diff_eq!(tx_max, m.max_angle_x, epsilon = 1e-12);
}
#[test]
fn test_beam_deflection() {
let m = make_tilt_mirror();
let angle = 0.1; let dist = 0.5; let deflection = m.beam_deflection(angle, dist);
assert_abs_diff_eq!(deflection, 2.0 * angle * dist, epsilon = 1e-15);
}
#[test]
fn test_bandwidth_3db() {
let m = make_tilt_mirror();
let bw = m.bandwidth_3db();
assert_abs_diff_eq!(bw, 5.0, epsilon = 1e-10);
}
#[test]
fn test_actuation_sensitivity() {
let m = make_tilt_mirror();
let sens = m.actuation_sensitivity();
assert_abs_diff_eq!(sens, 0.002, epsilon = 1e-10);
}
#[test]
fn test_gimbal_coupled_response() {
let inner = make_tilt_mirror();
let gimbal = GimbalMirror::new(inner, 200.0, 100.0);
let [tx, _ty] = gimbal.coupled_response(1000.0, 50.0);
assert!(tx > 0.0, "inner axis response should be positive");
}
#[test]
fn test_voa_zero_displacement() {
let voa = MemosVoa::new(1550e-9);
let att = voa.attenuation_db(0.0);
assert_abs_diff_eq!(att, 0.0, epsilon = 1e-10);
}
#[test]
fn test_voa_increasing_attenuation() {
let voa = MemosVoa::new(1550e-9);
let att1 = voa.attenuation_db(1e-6);
let att2 = voa.attenuation_db(5e-6);
assert!(
att2 > att1,
"larger displacement should give more attenuation"
);
}
#[test]
fn test_voa_displacement_for_attenuation() {
let voa = MemosVoa::new(1550e-9);
let target_db = 3.0;
let dx = voa
.displacement_for_attenuation(target_db)
.expect("should return a displacement for positive attenuation");
let att = voa.attenuation_db(dx);
assert_abs_diff_eq!(att, target_db, epsilon = 1e-6);
}
#[test]
fn test_frequency_response_at_resonance() {
let m = make_tilt_mirror();
let h = m.frequency_response_x(m.resonant_freq_x);
assert_abs_diff_eq!(h, m.q_factor_x, epsilon = 0.1);
}
}