use crate::error::OxiPhotonError;
type Mat3 = [[f64; 3]; 3];
fn mat3_mul(a: &Mat3, b: &Mat3) -> Mat3 {
let mut c = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn mat3_transpose(a: &Mat3) -> Mat3 {
let mut t = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
t[i][j] = a[j][i];
}
}
t
}
fn mat3_det(a: &Mat3) -> f64 {
a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
}
fn mat3_scale(a: &Mat3, s: f64) -> Mat3 {
let mut out = *a;
for row in &mut out {
for v in row.iter_mut() {
*v *= s;
}
}
out
}
const MAT3_IDENTITY: Mat3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
#[derive(Debug, Clone)]
pub enum TransformType {
CylindricalCloak {
inner_radius: f64,
outer_radius: f64,
},
SphericalCloak {
inner_radius: f64,
outer_radius: f64,
},
LinearScale {
scale_x: f64,
scale_y: f64,
scale_z: f64,
},
BeamBend {
bend_radius: f64,
angle_rad: f64,
},
Flattener {
curvature: f64,
},
}
#[derive(Debug, Clone)]
pub struct CoordTransformation {
pub transform_type: TransformType,
}
impl CoordTransformation {
pub fn new(transform_type: TransformType) -> Self {
Self { transform_type }
}
pub fn jacobian(&self, x: f64, y: f64, z: f64) -> Mat3 {
match &self.transform_type {
TransformType::LinearScale {
scale_x,
scale_y,
scale_z,
} => {
[
[*scale_x, 0.0, 0.0],
[0.0, *scale_y, 0.0],
[0.0, 0.0, *scale_z],
]
}
TransformType::CylindricalCloak {
inner_radius: a,
outer_radius: b,
} => {
let r = (x * x + y * y).sqrt();
if r < 1e-15 {
return MAT3_IDENTITY;
}
let a = *a;
let b = *b;
let dr_prime_dr = (b - a) / b;
let r_prime = a + r * (b - a) / b;
let cos_phi = x / r;
let sin_phi = y / r;
let jrr = dr_prime_dr;
let jss = r_prime / r; let jxx = jrr * cos_phi * cos_phi + jss * sin_phi * sin_phi;
let jxy = (jrr - jss) * sin_phi * cos_phi;
let jyx = jxy;
let jyy = jrr * sin_phi * sin_phi + jss * cos_phi * cos_phi;
[[jxx, jxy, 0.0], [jyx, jyy, 0.0], [0.0, 0.0, 1.0]]
}
TransformType::SphericalCloak {
inner_radius: a,
outer_radius: b,
} => {
let r = (x * x + y * y + z * z).sqrt();
if r < 1e-15 {
return MAT3_IDENTITY;
}
let a = *a;
let b = *b;
let dr_prime_dr = (b - a) / b;
let r_prime = a + r * (b - a) / b;
let ratio = r_prime / r;
let rx = x / r;
let ry = y / r;
let rz = z / r;
let diff = dr_prime_dr - ratio;
[
[ratio + diff * rx * rx, diff * rx * ry, diff * rx * rz],
[diff * ry * rx, ratio + diff * ry * ry, diff * ry * rz],
[diff * rz * rx, diff * rz * ry, ratio + diff * rz * rz],
]
}
TransformType::BeamBend {
bend_radius: r0,
angle_rad,
} => {
let s = x;
let u = y;
let r0 = *r0;
let raw_theta = s / r0;
let theta = raw_theta.clamp(-*angle_rad, *angle_rad);
let cos_t = theta.cos();
let sin_t = theta.sin();
let rho = r0 + u;
[
[-rho / r0 * sin_t, cos_t, 0.0],
[-rho / r0 * cos_t, -sin_t, 0.0],
[0.0, 0.0, 1.0],
]
}
TransformType::Flattener { curvature: kappa } => {
[
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[-kappa * x, -kappa * y, 1.0],
]
}
}
}
pub fn jacobian_det(&self, x: f64, y: f64, z: f64) -> f64 {
let j = self.jacobian(x, y, z);
mat3_det(&j)
}
pub fn transformed_eps(&self, x: f64, y: f64, z: f64) -> Mat3 {
let j = self.jacobian(x, y, z);
let det = mat3_det(&j);
if det.abs() < 1e-30 {
return MAT3_IDENTITY;
}
let jjt = mat3_mul(&j, &mat3_transpose(&j));
mat3_scale(&jjt, 1.0 / det)
}
pub fn transformed_mu(&self, x: f64, y: f64, z: f64) -> Mat3 {
self.transformed_eps(x, y, z)
}
pub fn virtual_to_physical(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
match &self.transform_type {
TransformType::LinearScale {
scale_x,
scale_y,
scale_z,
} => (x * scale_x, y * scale_y, z * scale_z),
TransformType::CylindricalCloak {
inner_radius: a,
outer_radius: b,
} => {
let rp = (x * x + y * y).sqrt();
if rp < *a {
return (x, y, z); }
let a = *a;
let b = *b;
let r = (rp - a) * b / (b - a);
let phi = y.atan2(x);
(r * phi.cos(), r * phi.sin(), z)
}
TransformType::SphericalCloak {
inner_radius: a,
outer_radius: b,
} => {
let rp = (x * x + y * y + z * z).sqrt();
if rp < *a {
return (x, y, z);
}
let a = *a;
let b = *b;
let r = (rp - a) * b / (b - a);
let scale = if rp > 1e-15 { r / rp } else { 1.0 };
(x * scale, y * scale, z * scale)
}
TransformType::BeamBend {
bend_radius: r0,
angle_rad,
} => {
let r0 = *r0;
let s = x;
let u = y;
let theta = (s / r0).clamp(-*angle_rad, *angle_rad);
let rho = r0 + u;
(rho * theta.cos(), -rho * theta.sin(), z)
}
TransformType::Flattener { curvature: kappa } => {
(x, y, z + kappa * (x * x + y * y) / 2.0)
}
}
}
pub fn physical_to_virtual(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
match &self.transform_type {
TransformType::LinearScale {
scale_x,
scale_y,
scale_z,
} => (x / scale_x, y / scale_y, z / scale_z),
TransformType::CylindricalCloak {
inner_radius: a,
outer_radius: b,
} => {
let r = (x * x + y * y).sqrt();
let a = *a;
let b = *b;
let rp = a + r * (b - a) / b;
let phi = y.atan2(x);
(rp * phi.cos(), rp * phi.sin(), z)
}
TransformType::SphericalCloak {
inner_radius: a,
outer_radius: b,
} => {
let r = (x * x + y * y + z * z).sqrt();
let a = *a;
let b = *b;
let rp = a + r * (b - a) / b;
let scale = if r > 1e-15 { rp / r } else { 1.0 };
(x * scale, y * scale, z * scale)
}
TransformType::BeamBend {
bend_radius: r0,
angle_rad,
} => {
let r0 = *r0;
let rho = (x * x + y * y).sqrt();
let theta = (-y).atan2(x).clamp(-*angle_rad, *angle_rad);
let s = r0 * theta;
let u = rho - r0;
(s, u, z)
}
TransformType::Flattener { curvature: kappa } => {
(x, y, z - kappa * (x * x + y * y) / 2.0)
}
}
}
}
#[derive(Debug, Clone)]
pub struct CylindricalCloak {
pub inner_radius: f64,
pub outer_radius: f64,
}
impl CylindricalCloak {
pub fn new(inner_radius: f64, outer_radius: f64) -> Result<Self, OxiPhotonError> {
if inner_radius <= 0.0 || outer_radius <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"Cloak radii must be positive".to_string(),
));
}
if inner_radius >= outer_radius {
return Err(OxiPhotonError::NumericalError(format!(
"inner_radius ({inner_radius}) must be < outer_radius ({outer_radius})"
)));
}
Ok(Self {
inner_radius,
outer_radius,
})
}
pub fn eps_radial(&self, r: f64) -> f64 {
let a = self.inner_radius;
(r - a) / r
}
pub fn eps_azimuthal(&self, r: f64) -> f64 {
let a = self.inner_radius;
if (r - a).abs() < 1e-30 {
return f64::INFINITY;
}
r / (r - a)
}
pub fn eps_z(&self, r: f64) -> f64 {
let a = self.inner_radius;
let b = self.outer_radius;
let scale = b / (b - a);
scale * scale * (r - a) / r
}
pub fn is_cloaked(&self, x: f64, y: f64) -> bool {
(x * x + y * y).sqrt() < self.inner_radius
}
#[allow(clippy::too_many_arguments)]
pub fn fill_fdtd_grid(
&self,
eps_r: &mut [f64],
eps_phi: &mut [f64],
eps_z: &mut [f64],
nx: usize,
ny: usize,
dx: f64,
dy: f64,
x_center: f64,
y_center: f64,
) {
let a = self.inner_radius;
let b = self.outer_radius;
for iy in 0..ny {
for ix in 0..nx {
let cx = (ix as f64 + 0.5) * dx - x_center;
let cy = (iy as f64 + 0.5) * dy - y_center;
let r = (cx * cx + cy * cy).sqrt();
let idx = iy * nx + ix;
if r >= a && r <= b {
eps_r[idx] = self.eps_radial(r);
eps_phi[idx] = self.eps_azimuthal(r);
eps_z[idx] = self.eps_z(r);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct EmConcentrator {
pub inner_radius: f64,
pub outer_radius: f64,
pub concentration_factor: f64,
}
impl EmConcentrator {
pub fn new(inner: f64, outer: f64, factor: f64) -> Result<Self, OxiPhotonError> {
if inner <= 0.0 || outer <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"Concentrator radii must be positive".to_string(),
));
}
if inner >= outer {
return Err(OxiPhotonError::NumericalError(format!(
"inner ({inner}) must be < outer ({outer})"
)));
}
if factor <= 1.0 {
return Err(OxiPhotonError::NumericalError(format!(
"concentration_factor must be > 1, got {factor}"
)));
}
Ok(Self {
inner_radius: inner,
outer_radius: outer,
concentration_factor: factor,
})
}
pub fn eps_radial(&self, r: f64) -> f64 {
let a = self.inner_radius;
let eta = self.concentration_factor;
let r_prime = eta * (r - a) + a;
if r_prime.abs() < 1e-30 {
return 0.0;
}
r / r_prime
}
pub fn eps_azimuthal(&self, r: f64) -> f64 {
let a = self.inner_radius;
let eta = self.concentration_factor;
let r_prime = eta * (r - a) + a;
if r.abs() < 1e-30 {
return 0.0;
}
r_prime / r
}
pub fn field_enhancement(&self) -> f64 {
let ratio = self.outer_radius / self.inner_radius;
ratio * ratio
}
}
#[derive(Debug, Clone)]
pub struct GradedIndexARC {
pub n_substrate: f64,
pub n_ambient: f64,
pub n_layers: usize,
pub total_thickness_nm: f64,
pub wavelength_nm: f64,
}
impl GradedIndexARC {
pub fn new(
n_substrate: f64,
n_ambient: f64,
n_layers: usize,
thickness_nm: f64,
lambda_nm: f64,
) -> Self {
Self {
n_substrate,
n_ambient,
n_layers: n_layers.max(2),
total_thickness_nm: thickness_nm,
wavelength_nm: lambda_nm,
}
}
pub fn index_profile(&self) -> Vec<f64> {
let nl = self.n_layers;
let ratio = self.n_substrate / self.n_ambient;
(0..nl)
.map(|i| {
let t = (i as f64 + 0.5) / nl as f64;
self.n_ambient * ratio.powf(t)
})
.collect()
}
pub fn quintic_index_profile(&self) -> Vec<f64> {
let nl = self.n_layers;
let n0 = self.n_ambient;
let ns = self.n_substrate;
(0..nl)
.map(|i| {
let t = i as f64 / (nl.saturating_sub(1).max(1)) as f64;
let p = 10.0 * t.powi(3) - 15.0 * t.powi(4) + 6.0 * t.powi(5);
n0 * (ns / n0).powf(p)
})
.collect()
}
pub fn fill_fractions(&self, n_inclusion: f64) -> Vec<f64> {
let eps_host = num_complex::Complex64::new(self.n_ambient * self.n_ambient, 0.0);
let eps_incl = num_complex::Complex64::new(n_inclusion * n_inclusion, 0.0);
self.index_profile()
.iter()
.map(|&n_target| {
let eps_target = n_target * n_target;
let eh = eps_host.re;
let ei = eps_incl.re;
let et = eps_target;
let delta = ei - eh;
if delta.abs() < 1e-12 {
return 0.0; }
let num = (et - eh) * (ei + 2.0 * eh);
let _den = (et - eh) * delta + 3.0 * delta * eh;
let den = delta * (et + 2.0 * eh);
if den.abs() < 1e-12 {
return 0.0;
}
(num / den).clamp(0.0, 1.0)
})
.collect()
}
pub fn layer_thicknesses_nm(&self) -> Vec<f64> {
let d = self.total_thickness_nm / self.n_layers as f64;
vec![d; self.n_layers]
}
pub fn reflectance(&self) -> f64 {
let profile = self.index_profile();
let thicknesses = self.layer_thicknesses_nm();
let lambda = self.wavelength_nm;
use num_complex::Complex64;
let i_unit = Complex64::new(0.0, 1.0);
let mut m = [[Complex64::new(1.0, 0.0); 2]; 2];
let all_n: Vec<f64> = std::iter::once(self.n_ambient)
.chain(profile.iter().copied())
.chain(std::iter::once(self.n_substrate))
.collect();
for (k, (&n_layer, &d)) in profile.iter().zip(thicknesses.iter()).enumerate() {
let delta = 2.0 * std::f64::consts::PI * n_layer * d / lambda;
let cos_d = Complex64::new(delta.cos(), 0.0);
let sin_d = Complex64::new(delta.sin(), 0.0);
let n_c = Complex64::new(n_layer, 0.0);
let ml = [
[cos_d, -i_unit * sin_d / n_c],
[-i_unit * n_c * sin_d, cos_d],
];
let m00 = ml[0][0] * m[0][0] + ml[0][1] * m[1][0];
let m01 = ml[0][0] * m[0][1] + ml[0][1] * m[1][1];
let m10 = ml[1][0] * m[0][0] + ml[1][1] * m[1][0];
let m11 = ml[1][0] * m[0][1] + ml[1][1] * m[1][1];
m = [[m00, m01], [m10, m11]];
let _ = (k, all_n[k + 1]); }
let n0 = Complex64::new(self.n_ambient, 0.0);
let ns = Complex64::new(self.n_substrate, 0.0);
let num = m[0][0] * n0 + m[0][1] * n0 * ns - m[1][0] - m[1][1] * ns;
let den = m[0][0] * n0 + m[0][1] * n0 * ns + m[1][0] + m[1][1] * ns;
if den.norm() < 1e-30 {
return 0.0;
}
(num / den).norm_sqr()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_cloak_inner_eps_r() {
let a = 0.1_f64;
let b = 0.3_f64;
let cloak = CylindricalCloak::new(a, b).unwrap();
let r_near = a + 1e-8;
let eps_r = cloak.eps_radial(r_near);
assert!(
eps_r.abs() < 1e-6,
"eps_r should approach 0 near inner radius, got {eps_r}"
);
}
#[test]
fn test_cloak_outer_eps() {
let a = 0.1_f64;
let b = 0.3_f64;
let cloak = CylindricalCloak::new(a, b).unwrap();
let eps_r = cloak.eps_radial(b);
let eps_phi = cloak.eps_azimuthal(b);
assert_abs_diff_eq!(eps_r * eps_phi, 1.0, epsilon = 1e-12);
let eps_z = cloak.eps_z(b);
let expected_ez = b / (b - a);
assert_abs_diff_eq!(eps_z, expected_ez, epsilon = 1e-12);
}
#[test]
fn test_cloak_hidden_region() {
let cloak = CylindricalCloak::new(0.1, 0.3).unwrap();
assert!(cloak.is_cloaked(0.0, 0.05)); assert!(!cloak.is_cloaked(0.2, 0.0)); assert!(!cloak.is_cloaked(0.5, 0.0)); }
#[test]
fn test_cloak_invalid_radii() {
assert!(CylindricalCloak::new(0.3, 0.1).is_err()); assert!(CylindricalCloak::new(-0.1, 0.3).is_err()); assert!(CylindricalCloak::new(0.1, 0.1).is_err()); }
#[test]
fn test_concentrator_field_enhancement() {
let conc = EmConcentrator::new(0.05, 0.2, 2.0).unwrap();
let fe = conc.field_enhancement();
assert!(fe > 1.0, "field enhancement should be > 1, got {fe}");
assert_abs_diff_eq!(fe, 16.0, epsilon = 1e-10);
}
#[test]
fn test_concentrator_invalid_params() {
assert!(EmConcentrator::new(0.2, 0.1, 2.0).is_err()); assert!(EmConcentrator::new(0.1, 0.2, 0.5).is_err()); }
#[test]
fn test_arc_index_profile_bounds() {
let arc = GradedIndexARC::new(3.5, 1.0, 20, 200.0, 550.0);
let profile = arc.index_profile();
assert_eq!(profile.len(), 20);
for &n in &profile {
assert!(
n >= arc.n_ambient * 0.9999 && n <= arc.n_substrate * 1.0001,
"n={n} out of bounds [{}, {}]",
arc.n_ambient,
arc.n_substrate
);
}
}
#[test]
fn test_arc_quintic_smooth() {
let arc = GradedIndexARC::new(3.5, 1.0, 21, 200.0, 550.0);
let profile = arc.quintic_index_profile();
assert_eq!(profile.len(), 21);
assert_abs_diff_eq!(profile[0], arc.n_ambient, epsilon = 1e-12);
assert_abs_diff_eq!(profile[20], arc.n_substrate, epsilon = 1e-10);
}
#[test]
fn test_jacobian_determinant_positive() {
let transforms = [
CoordTransformation::new(TransformType::LinearScale {
scale_x: 2.0,
scale_y: 1.5,
scale_z: 1.0,
}),
CoordTransformation::new(TransformType::CylindricalCloak {
inner_radius: 0.1,
outer_radius: 0.3,
}),
CoordTransformation::new(TransformType::BeamBend {
bend_radius: 1.0,
angle_rad: std::f64::consts::PI / 4.0,
}),
];
for (t, xform) in transforms.iter().enumerate() {
let det = xform.jacobian_det(0.2, 0.15, 0.0);
assert!(det > 0.0, "transform {t}: det(J)={det} should be positive");
}
}
#[test]
fn test_linear_scale_jacobian() {
let sx = 3.0;
let sy = 2.0;
let sz = 1.5;
let xform = CoordTransformation::new(TransformType::LinearScale {
scale_x: sx,
scale_y: sy,
scale_z: sz,
});
let j = xform.jacobian(1.0, 1.0, 1.0);
assert_abs_diff_eq!(j[0][0], sx, epsilon = 1e-14);
assert_abs_diff_eq!(j[1][1], sy, epsilon = 1e-14);
assert_abs_diff_eq!(j[2][2], sz, epsilon = 1e-14);
assert_abs_diff_eq!(j[0][1], 0.0, epsilon = 1e-14);
assert_abs_diff_eq!(j[1][0], 0.0, epsilon = 1e-14);
let det = xform.jacobian_det(0.0, 0.0, 0.0);
assert_abs_diff_eq!(det, sx * sy * sz, epsilon = 1e-12);
}
#[test]
fn test_cylindrical_cloak_jacobian_det_positive_in_shell() {
let a = 0.1_f64;
let b = 0.3_f64;
let xform = CoordTransformation::new(TransformType::CylindricalCloak {
inner_radius: a,
outer_radius: b,
});
for r_frac in [0.1, 0.3, 0.5, 0.7, 0.9] {
let r = a + r_frac * (b - a);
let det = xform.jacobian_det(r, 0.0, 0.0);
assert!(det > 0.0, "det(J)={det} at r={r} should be positive");
}
}
#[test]
fn test_spherical_cloak_eps_symmetric() {
let xform = CoordTransformation::new(TransformType::SphericalCloak {
inner_radius: 0.1,
outer_radius: 0.3,
});
let eps = xform.transformed_eps(0.15, 0.1, 0.05);
for (i, row) in eps.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert_abs_diff_eq!(val, eps[j][i], epsilon = 1e-12);
}
}
}
}