use num_complex::Complex64;
use oxifft::Complex as OxiComplex;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircPolarization {
LCP,
RCP,
}
impl CircPolarization {
pub fn jones_vector(&self) -> [Complex64; 2] {
let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
match self {
CircPolarization::LCP => [
Complex64::new(inv_sqrt2, 0.0),
Complex64::new(0.0, inv_sqrt2),
],
CircPolarization::RCP => [
Complex64::new(inv_sqrt2, 0.0),
Complex64::new(0.0, -inv_sqrt2),
],
}
}
pub fn opposite(&self) -> CircPolarization {
match self {
CircPolarization::LCP => CircPolarization::RCP,
CircPolarization::RCP => CircPolarization::LCP,
}
}
}
#[derive(Debug, Clone)]
pub struct PbPhaseElement {
pub n_pixels_x: usize,
pub n_pixels_y: usize,
pub pixel_size: f64,
pub orientation_map: Vec<Vec<f64>>,
pub wavelength: f64,
pub conversion_efficiency: f64,
}
impl PbPhaseElement {
pub fn new(nx: usize, ny: usize, pixel_size: f64, wavelength: f64) -> Self {
Self {
n_pixels_x: nx,
n_pixels_y: ny,
pixel_size,
orientation_map: vec![vec![0.0; nx]; ny],
wavelength,
conversion_efficiency: 1.0,
}
}
pub fn phase_at(&self, i: usize, j: usize) -> f64 {
if j < self.orientation_map.len() && i < self.orientation_map[j].len() {
2.0 * self.orientation_map[j][i]
} else {
0.0
}
}
fn pixel_x(&self, i: usize) -> f64 {
(i as f64 - (self.n_pixels_x as f64 - 1.0) * 0.5) * self.pixel_size
}
fn pixel_y(&self, j: usize) -> f64 {
(j as f64 - (self.n_pixels_y as f64 - 1.0) * 0.5) * self.pixel_size
}
pub fn set_lens_profile(&mut self, focal_length: f64) {
for j in 0..self.n_pixels_y {
for i in 0..self.n_pixels_x {
let x = self.pixel_x(i);
let y = self.pixel_y(j);
let r2 = x * x + y * y;
self.orientation_map[j][i] = -PI * r2 / (self.wavelength * focal_length);
}
}
}
pub fn set_grating_profile(&mut self, period_m: f64) {
for j in 0..self.n_pixels_y {
for i in 0..self.n_pixels_x {
let x = self.pixel_x(i);
self.orientation_map[j][i] = PI * x / period_m;
}
}
}
pub fn set_vortex_profile(&mut self, topological_charge: i32) {
let l = topological_charge as f64;
for j in 0..self.n_pixels_y {
for i in 0..self.n_pixels_x {
let x = self.pixel_x(i);
let y = self.pixel_y(j);
self.orientation_map[j][i] = (l / 2.0) * y.atan2(x);
}
}
}
pub fn apply(
&self,
input: &[Vec<Complex64>],
input_polarization: CircPolarization,
) -> Vec<Vec<Complex64>> {
let ny = input.len();
let nx = if ny > 0 { input[0].len() } else { 0 };
let sign = match input_polarization {
CircPolarization::LCP => 1.0_f64,
CircPolarization::RCP => -1.0_f64,
};
let sqrt_eff = self.conversion_efficiency.sqrt();
let mut out = vec![vec![Complex64::new(0.0, 0.0); nx]; ny];
for j in 0..ny.min(self.n_pixels_y) {
for i in 0..nx.min(self.n_pixels_x) {
let theta = self.orientation_map[j][i];
let phase = sign * 2.0 * theta;
let phasor = Complex64::from_polar(sqrt_eff, phase);
out[j][i] = input[j][i] * phasor;
}
}
out
}
pub fn efficiency_in_order(&self, order: i32) -> f64 {
match order {
0 => 1.0 - self.conversion_efficiency,
1 | -1 => self.conversion_efficiency / 2.0,
_ => 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PbBeamSplitter {
pub grating_period: f64,
pub wavelength: f64,
pub deflection_angle_deg: f64,
}
impl PbBeamSplitter {
pub fn new(period: f64, wavelength: f64) -> Self {
let angle_rad = (wavelength / period).clamp(-1.0, 1.0).asin();
Self {
grating_period: period,
wavelength,
deflection_angle_deg: angle_rad.to_degrees(),
}
}
pub fn deflection_angle_deg(&self) -> f64 {
(self.wavelength / self.grating_period)
.clamp(-1.0, 1.0)
.asin()
.to_degrees()
}
pub fn polarization_separation_angle(&self) -> f64 {
2.0 * self.deflection_angle_deg()
}
pub fn efficiency_per_order(&self) -> f64 {
0.5 }
}
#[derive(Debug, Clone)]
pub enum MetasurfaceFunction {
Lens {
focal_length: f64,
},
Grating {
period: f64,
angle_deg: f64,
},
Hologram {
phase_map: std::sync::Arc<Vec<Vec<f64>>>,
origin: (f64, f64),
pitch: f64,
label: String,
},
Vortex {
charge: i32,
},
}
impl MetasurfaceFunction {
pub fn orientation_at(&self, x: f64, y: f64, wavelength: f64) -> f64 {
match self {
MetasurfaceFunction::Lens { focal_length } => {
let r2 = x * x + y * y;
-PI * r2 / (wavelength * focal_length)
}
MetasurfaceFunction::Grating { period, angle_deg } => {
let dir = angle_deg.to_radians();
let s = x * dir.cos() + y * dir.sin();
PI * s / period
}
MetasurfaceFunction::Hologram {
phase_map,
origin,
pitch,
..
} => {
if phase_map.is_empty() || phase_map[0].is_empty() {
return 0.0;
}
let ny = phase_map.len();
let nx = phase_map[0].len();
let fx = (x - origin.0) / pitch;
let fy = (y - origin.1) / pitch;
if fx < 0.0 || fy < 0.0 || fx >= nx as f64 || fy >= ny as f64 {
return 0.0;
}
let ix0 = fx.floor() as usize;
let iy0 = fy.floor() as usize;
let ix1 = (ix0 + 1).min(nx - 1);
let iy1 = (iy0 + 1).min(ny - 1);
let tx = fx - ix0 as f64;
let ty = fy - iy0 as f64;
let p00 = phase_map[iy0][ix0];
let p10 = phase_map[iy0][ix1];
let p01 = phase_map[iy1][ix0];
let p11 = phase_map[iy1][ix1];
let phase = p00 * (1.0 - tx) * (1.0 - ty)
+ p10 * tx * (1.0 - ty)
+ p01 * (1.0 - tx) * ty
+ p11 * tx * ty;
phase / 2.0
}
MetasurfaceFunction::Vortex { charge } => {
let l = *charge as f64;
(l / 2.0) * y.atan2(x)
}
}
}
pub fn hologram_from_phase_map(
phase_map: Vec<Vec<f64>>,
origin: (f64, f64),
pitch: f64,
label: impl Into<String>,
) -> Result<Self, String> {
if phase_map.is_empty() {
return Err("phase_map must not be empty".into());
}
if pitch <= 0.0 {
return Err(format!("pitch must be positive, got {pitch}"));
}
let ncols = phase_map[0].len();
if ncols == 0 {
return Err("phase_map rows must not be empty".into());
}
if phase_map.iter().any(|row| row.len() != ncols) {
return Err("phase_map must be rectangular (all rows same length)".into());
}
Ok(MetasurfaceFunction::Hologram {
phase_map: std::sync::Arc::new(phase_map),
origin,
pitch,
label: label.into(),
})
}
pub fn hologram_from_target(
target: &[Vec<f64>],
pitch: f64,
n_iters: usize,
label: impl Into<String>,
) -> Result<Self, String> {
if target.is_empty() || target[0].is_empty() {
return Err("target must be non-empty".into());
}
if pitch <= 0.0 {
return Err(format!("pitch must be positive, got {pitch}"));
}
let ny = target.len();
let nx = target[0].len();
if target.iter().any(|row| row.len() != nx) {
return Err("target must be rectangular".into());
}
let target_amp: Vec<Vec<f64>> = target
.iter()
.map(|row| row.iter().map(|&v| v.max(0.0).sqrt()).collect())
.collect();
let n_total = ny * nx;
let mut pupil: Vec<OxiComplex<f64>> = (0..n_total)
.map(|k| {
let phase = PI * (k as f64) / (n_total as f64);
OxiComplex::from_polar(1.0_f64, phase)
})
.collect();
for _ in 0..n_iters {
let mut image = oxifft::fft2d(&pupil, ny, nx);
for (iy, row) in target_amp.iter().enumerate() {
for (ix, &_t) in row.iter().enumerate() {
let c = image[iy * nx + ix];
let ph = c.arg();
image[iy * nx + ix] = OxiComplex::from_polar(amp_t, ph);
}
}
let back = oxifft::ifft2d(&image, ny, nx);
for (k, c) in back.iter().enumerate() {
let ph = c.arg();
pupil[k] = OxiComplex::from_polar(1.0_f64, ph);
}
}
let phase_map: Vec<Vec<f64>> = (0..ny)
.map(|iy| (0..nx).map(|ix| pupil[iy * nx + ix].arg()).collect())
.collect();
Self::hologram_from_phase_map(phase_map, (0.0, 0.0), pitch, label)
}
}
#[derive(Debug, Clone)]
pub struct SpinMultiplexedMetasurface {
pub lcp_function: MetasurfaceFunction,
pub rcp_function: MetasurfaceFunction,
pub n_pixels: usize,
pub pixel_size: f64,
}
impl SpinMultiplexedMetasurface {
pub fn new(
lcp: MetasurfaceFunction,
rcp: MetasurfaceFunction,
n: usize,
pixel_size: f64,
) -> Self {
Self {
lcp_function: lcp,
rcp_function: rcp,
n_pixels: n,
pixel_size,
}
}
fn coord(&self, k: usize) -> f64 {
(k as f64 - (self.n_pixels as f64 - 1.0) * 0.5) * self.pixel_size
}
pub fn phase_at_pixel(&self, i: usize, j: usize, polarization: CircPolarization) -> f64 {
let wavelength = 500e-9;
let x = self.coord(i);
let y = self.coord(j);
let theta_lcp = self.lcp_function.orientation_at(x, y, wavelength);
let theta_rcp = self.rcp_function.orientation_at(x, y, wavelength);
match polarization {
CircPolarization::LCP => 2.0 * theta_lcp,
CircPolarization::RCP => -2.0 * theta_rcp,
}
}
pub fn crosstalk_db(&self) -> f64 {
-20.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn circ_pol_jones_vectors_unit_norm() {
for pol in [CircPolarization::LCP, CircPolarization::RCP] {
let jv = pol.jones_vector();
let norm_sq = jv[0].norm_sqr() + jv[1].norm_sqr();
assert_abs_diff_eq!(norm_sq, 1.0, epsilon = 1e-12);
}
}
#[test]
fn pb_element_phase_at_centre_is_zero_for_flat_profile() {
let elem = PbPhaseElement::new(11, 11, 100e-9, 500e-9);
assert_abs_diff_eq!(elem.phase_at(5, 5), 0.0, epsilon = 1e-15);
}
#[test]
fn pb_element_geometric_phase_is_twice_orientation() {
let mut elem = PbPhaseElement::new(5, 5, 100e-9, 500e-9);
elem.orientation_map[2][2] = PI / 6.0; assert_abs_diff_eq!(elem.phase_at(2, 2), PI / 3.0, epsilon = 1e-14);
}
#[test]
fn pb_element_lens_profile_centre_angle_zero() {
let mut elem = PbPhaseElement::new(11, 11, 100e-9, 500e-9);
elem.set_lens_profile(1e-3);
assert_abs_diff_eq!(elem.orientation_map[5][5], 0.0, epsilon = 1e-12);
}
#[test]
fn pb_element_vortex_profile_charge_one() {
let mut elem = PbPhaseElement::new(11, 11, 100e-9, 500e-9);
elem.set_vortex_profile(2); assert_abs_diff_eq!(elem.orientation_map[5][10], 0.0, epsilon = 1e-12);
}
#[test]
fn pb_element_efficiency_orders_sum_to_one() {
let elem = PbPhaseElement::new(11, 11, 100e-9, 500e-9);
let sum = elem.efficiency_in_order(-1)
+ elem.efficiency_in_order(0)
+ elem.efficiency_in_order(1);
assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-12);
}
#[test]
fn pb_beam_splitter_deflection_angle() {
let bs = PbBeamSplitter::new(1000e-9, 500e-9);
assert_abs_diff_eq!(bs.deflection_angle_deg(), 30.0, epsilon = 1e-6);
assert_abs_diff_eq!(bs.polarization_separation_angle(), 60.0, epsilon = 1e-6);
}
#[test]
fn spin_multiplexed_lcp_rcp_different_phases() {
let lcp_fn = MetasurfaceFunction::Lens { focal_length: 1e-3 };
let rcp_fn = MetasurfaceFunction::Grating {
period: 2e-6,
angle_deg: 0.0,
};
let ms = SpinMultiplexedMetasurface::new(lcp_fn, rcp_fn, 21, 100e-9);
let phi_lcp = ms.phase_at_pixel(15, 10, CircPolarization::LCP);
let phi_rcp = ms.phase_at_pixel(15, 10, CircPolarization::RCP);
assert!(
(phi_lcp - phi_rcp).abs() > 1e-6,
"LCP and RCP phases are identical: phi_lcp={phi_lcp}, phi_rcp={phi_rcp}"
);
}
#[test]
fn hologram_phase_map_interpolation() {
let map = vec![
vec![0.0, PI / 4.0, PI / 2.0],
vec![PI, PI, PI],
vec![3.0 * PI / 2.0, 7.0 * PI / 4.0, 2.0 * PI],
];
let func = MetasurfaceFunction::hologram_from_phase_map(map, (0.0, 0.0), 1.0e-6, "test")
.expect("valid hologram");
let theta = func.orientation_at(1.0e-6, 1.0e-6, 1550e-9);
let expected = PI / 2.0; assert!(
(theta - expected).abs() < 1e-12,
"got {theta}, expected {expected}"
);
}
#[test]
fn hologram_from_phase_map_errors() {
assert!(
MetasurfaceFunction::hologram_from_phase_map(vec![], (0.0, 0.0), 1e-6, "").is_err()
);
assert!(
MetasurfaceFunction::hologram_from_phase_map(vec![vec![0.0]], (0.0, 0.0), 0.0, "")
.is_err()
);
}
#[test]
fn hologram_from_target_gs_convergence() {
let mut target = vec![vec![0.0f64; 8]; 8];
target[2][2] = 1.0;
let hologram = MetasurfaceFunction::hologram_from_target(&target, 1.0e-6, 50, "gs_test")
.expect("GS should succeed");
match &hologram {
MetasurfaceFunction::Hologram { phase_map, .. } => {
assert_eq!(phase_map.len(), 8);
assert_eq!(phase_map[0].len(), 8);
for row in phase_map.iter() {
for &phase in row.iter() {
assert!(
(-PI - 1e-10..=PI + 1e-10).contains(&phase),
"phase {phase} out of range"
);
}
}
}
_ => panic!("expected Hologram variant"),
}
}
}