use std::f64::consts::PI;
fn bessel_j1(x: f64) -> f64 {
const R1: [f64; 6] = [
72362614232.0,
-7895059235.0,
242396853.1,
-2972611.439,
15704.48260,
-30.16036606,
];
const S1: [f64; 6] = [
144725228442.0,
2300535178.0,
18583304.74,
99447.43394,
376.9991397,
1.0,
];
const P1: [f64; 5] = [
1.0,
0.183105e-2,
-0.3516396496e-4,
0.2457520174e-5,
-0.240337019e-6,
];
const Q1: [f64; 5] = [
0.04687499995,
-0.2002690873e-3,
0.8449199096e-5,
-0.88228987e-6,
0.105787412e-6,
];
if x == 0.0 {
return 0.0;
}
let ax = x.abs();
let sign = if x < 0.0 { -1.0 } else { 1.0 };
if ax < 8.0 {
let y = x * x;
let r = R1[0] + y * (R1[1] + y * (R1[2] + y * (R1[3] + y * (R1[4] + y * R1[5]))));
let s = S1[0] + y * (S1[1] + y * (S1[2] + y * (S1[3] + y * (S1[4] + y * S1[5]))));
sign * x * r / s
} else {
let z = 8.0 / ax;
let y = z * z;
let xx = ax - 2.356_194_490_192_345; let p = P1[0] + y * (P1[1] + y * (P1[2] + y * (P1[3] + y * P1[4])));
let q = Q1[0] + y * (Q1[1] + y * (Q1[2] + y * (Q1[3] + y * Q1[4])));
sign * (2.0 / (PI * ax)).sqrt() * (xx.cos() * p - z * xx.sin() * q)
}
}
#[derive(Debug, Clone)]
pub enum MetalensUnitCellType {
DielectricPillar {
n_pillar: f64,
height: f64,
period: f64,
},
GeometricPhase {
antenna_length: f64,
period: f64,
},
Resonant {
q_factor: f64,
},
}
impl MetalensUnitCellType {
pub fn theoretical_peak_efficiency(&self) -> f64 {
match self {
MetalensUnitCellType::DielectricPillar { .. } => 0.85,
MetalensUnitCellType::GeometricPhase { .. } => 0.50, MetalensUnitCellType::Resonant { q_factor } => {
(1.0 - 1.0 / q_factor).clamp(0.0, 0.99)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Metalens {
pub focal_length: f64,
pub diameter: f64,
pub wavelength: f64,
pub n_a: f64,
pub unit_cell: MetalensUnitCellType,
}
impl Metalens {
pub fn new(focal_length: f64, diameter: f64, wavelength: f64) -> Self {
let half_angle = (diameter / 2.0 / focal_length).atan();
let na = half_angle.sin();
Self {
focal_length,
diameter,
wavelength,
n_a: na,
unit_cell: MetalensUnitCellType::DielectricPillar {
n_pillar: 2.4,
height: 600e-9,
period: 350e-9,
},
}
}
pub fn required_phase(&self, r: f64) -> f64 {
let f = self.focal_length;
-(2.0 * PI / self.wavelength) * ((r * r + f * f).sqrt() - f)
}
pub fn numerical_aperture(&self) -> f64 {
let half_angle = (self.diameter / 2.0 / self.focal_length).atan();
half_angle.sin()
}
pub fn focal_spot_size(&self) -> f64 {
self.wavelength / (2.0 * self.numerical_aperture())
}
pub fn depth_of_focus(&self) -> f64 {
let na = self.numerical_aperture();
self.wavelength / (2.0 * na * na)
}
pub fn n_fresnel_zones(&self) -> usize {
let n = (self.diameter * self.diameter) / (4.0 * self.wavelength * self.focal_length);
n.ceil() as usize
}
pub fn chromatic_aberration_um_per_nm(&self) -> f64 {
-self.focal_length / self.wavelength * 1e-3
}
pub fn efficiency(&self) -> f64 {
self.unit_cell.theoretical_peak_efficiency()
}
pub fn psf_intensity(&self, r: f64) -> f64 {
if r == 0.0 {
return 1.0;
}
let na = self.numerical_aperture();
let u = 2.0 * PI * na * r / self.wavelength;
let airy = 2.0 * bessel_j1(u) / u;
airy * airy
}
pub fn strehl_ratio(&self, n_phase_levels: usize) -> f64 {
if n_phase_levels == 0 {
return 0.0;
}
let n = n_phase_levels as f64;
let sinc_val = (PI / n).sin() / (PI / n);
sinc_val * sinc_val
}
}
#[derive(Debug, Clone)]
pub struct MetalensDoublet {
pub lens1: Metalens,
pub lens2: Metalens,
pub separation: f64,
}
impl MetalensDoublet {
pub fn new_achromat(focal_length: f64, diameter: f64, wavelength: f64) -> Self {
let mut lens1 = Metalens::new(2.0 * focal_length, diameter, wavelength);
lens1.unit_cell = MetalensUnitCellType::DielectricPillar {
n_pillar: 2.4,
height: 600e-9,
period: 350e-9,
};
let mut lens2 = Metalens::new(-2.0 * focal_length, diameter, wavelength);
lens2.unit_cell = MetalensUnitCellType::Resonant { q_factor: 50.0 };
Self {
lens1,
lens2,
separation: 0.0,
}
}
pub fn effective_focal_length(&self) -> f64 {
let f1 = self.lens1.focal_length;
let f2 = self.lens2.focal_length;
let d = self.separation;
let inv_f = 1.0 / f1 + 1.0 / f2 - d / (f1 * f2);
if inv_f.abs() < 1e-30 {
f64::INFINITY
} else {
1.0 / inv_f
}
}
pub fn chromatic_correction_factor(&self) -> f64 {
let f_eff = self.effective_focal_length();
if !f_eff.is_finite() {
return 1.0;
}
let chrom1 = self.lens1.chromatic_aberration_um_per_nm().abs();
let chrom2 = self.lens2.chromatic_aberration_um_per_nm().abs();
let net = (chrom1 - chrom2).abs();
let single = self.lens1.chromatic_aberration_um_per_nm().abs();
if single < 1e-30 {
1.0
} else {
(net / single).clamp(0.0, 1.0)
}
}
pub fn combined_na(&self) -> f64 {
self.lens1.numerical_aperture()
}
}
#[derive(Debug, Clone)]
pub enum TuningMechanism {
LiquidCrystal {
max_phase_shift: f64,
},
Microfluidic {
tuning_range_mm: f64,
},
ThermoOptic {
dn_dt: f64,
delta_t_max: f64,
},
Mechanical {
max_strain: f64,
},
}
impl TuningMechanism {
pub fn focal_length_tuning_fraction(&self) -> f64 {
match self {
TuningMechanism::LiquidCrystal { max_phase_shift } => {
let n_zones = 10.0_f64;
max_phase_shift / (2.0 * PI * n_zones)
}
TuningMechanism::Microfluidic { tuning_range_mm } => {
tuning_range_mm * 1e-3 / 0.010
}
TuningMechanism::ThermoOptic { dn_dt, delta_t_max } => {
(dn_dt * delta_t_max / 1.5).abs()
}
TuningMechanism::Mechanical { max_strain } => {
max_strain.abs()
}
}
}
}
#[derive(Debug, Clone)]
pub struct VarifocalMetalens {
pub base_focal_length: f64,
pub wavelength: f64,
pub diameter: f64,
pub tuning_mechanism: TuningMechanism,
}
impl VarifocalMetalens {
pub fn new(f0: f64, wavelength: f64, diameter: f64, mechanism: TuningMechanism) -> Self {
Self {
base_focal_length: f0,
wavelength,
diameter,
tuning_mechanism: mechanism,
}
}
pub fn tunable_range_mm(&self) -> (f64, f64) {
let delta_frac = self.tuning_mechanism.focal_length_tuning_fraction();
let f0 = self.base_focal_length;
let delta = f0 * delta_frac;
let f_min = (f0 - delta).max(1e-6);
let f_max = f0 + delta;
(f_min, f_max)
}
pub fn efficiency_at_tuning(&self, tuning_fraction: f64) -> f64 {
let tau = tuning_fraction.clamp(0.0, 1.0);
let eta0 = 0.80_f64; let kappa = match &self.tuning_mechanism {
TuningMechanism::LiquidCrystal { .. } => 0.15,
TuningMechanism::Microfluidic { .. } => 0.05,
TuningMechanism::ThermoOptic { .. } => 0.25,
TuningMechanism::Mechanical { .. } => 0.30,
};
(eta0 * (1.0 - kappa * tau * tau)).clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn metalens_required_phase_zero_at_centre() {
let lens = Metalens::new(1e-3, 500e-6, 532e-9);
assert_abs_diff_eq!(lens.required_phase(0.0), 0.0, epsilon = 1e-15);
}
#[test]
fn metalens_na_consistent_with_diameter_and_focal_length() {
let lens = Metalens::new(1e-3, 2e-3, 532e-9);
let expected = 45.0_f64.to_radians().sin();
assert_abs_diff_eq!(lens.numerical_aperture(), expected, epsilon = 1e-10);
}
#[test]
fn metalens_psf_peak_at_centre() {
let lens = Metalens::new(1e-3, 500e-6, 532e-9);
assert_abs_diff_eq!(lens.psf_intensity(0.0), 1.0, epsilon = 1e-10);
}
#[test]
fn metalens_psf_first_dark_ring() {
let wavelength = 500e-9_f64;
let na = 0.5_f64;
let f = 1e-3_f64;
let d = 2.0 * f * na.asin().tan();
let lens = Metalens::new(f, d, wavelength);
let r1 = 3.8317 * wavelength / (2.0 * PI * na);
let psf = lens.psf_intensity(r1);
assert!(psf < 0.05, "PSF at first dark ring = {psf} (expected ≈ 0)");
}
#[test]
fn metalens_strehl_ratio_limit_cases() {
let lens = Metalens::new(1e-3, 500e-6, 532e-9);
assert_abs_diff_eq!(lens.strehl_ratio(1024), 1.0, epsilon = 5e-5);
assert_abs_diff_eq!(lens.strehl_ratio(0), 0.0, epsilon = 1e-15);
let expected = (2.0 / PI) * (2.0 / PI);
assert_abs_diff_eq!(lens.strehl_ratio(2), expected, epsilon = 1e-6);
}
#[test]
fn doublet_effective_focal_length() {
let doublet = MetalensDoublet::new_achromat(1e-3, 500e-6, 532e-9);
let f_eff = doublet.effective_focal_length();
assert!(!f_eff.is_nan());
}
#[test]
fn varifocal_lc_range_is_centred_on_base() {
let mech = TuningMechanism::LiquidCrystal {
max_phase_shift: 2.0 * PI,
};
let vlens = VarifocalMetalens::new(1e-3, 532e-9, 500e-6, mech);
let (f_min, f_max) = vlens.tunable_range_mm();
assert!(f_min < vlens.base_focal_length);
assert!(f_max > vlens.base_focal_length);
}
#[test]
fn varifocal_efficiency_at_zero_tuning() {
let mech = TuningMechanism::Mechanical { max_strain: 0.2 };
let vlens = VarifocalMetalens::new(1e-3, 532e-9, 500e-6, mech);
assert_abs_diff_eq!(vlens.efficiency_at_tuning(0.0), 0.80, epsilon = 1e-10);
}
#[test]
fn bessel_j1_known_values() {
assert_abs_diff_eq!(bessel_j1(0.0), 0.0, epsilon = 1e-15);
assert_abs_diff_eq!(bessel_j1(1.0), 0.440_050_585, epsilon = 1e-5);
let first_zero = bessel_j1(3.831_7);
assert!(first_zero.abs() < 0.01, "J₁(3.8317) = {first_zero}");
}
}