use std::f64::consts::PI;
pub fn bessel_j1(x: f64) -> f64 {
if x < 0.0 {
return -bessel_j1(-x);
}
if x == 0.0 {
return 0.0;
}
if x < 5.0 {
let x_half_sq = (x / 2.0) * (x / 2.0);
let mut term = x / 2.0; let mut sum = term;
for k in 1..=50_usize {
term *= -x_half_sq / (k as f64 * (k + 1) as f64);
sum += term;
if term.abs() < 1e-16 * sum.abs() {
break;
}
}
sum
} else {
(2.0 / (PI * x)).sqrt() * (x - 3.0 * PI / 4.0).cos()
}
}
pub fn recip_lattice(a1: [f64; 2], a2: [f64; 2]) -> ([f64; 2], [f64; 2]) {
let cross = a1[0] * a2[1] - a1[1] * a2[0];
let b1 = [2.0 * PI * a2[1] / cross, -2.0 * PI * a2[0] / cross];
let b2 = [-2.0 * PI * a1[1] / cross, 2.0 * PI * a1[0] / cross];
(b1, b2)
}
pub fn gen_gvecs(b1: [f64; 2], b2: [f64; 2], n_max: i32) -> Vec<[f64; 2]> {
let mag_b1 = (b1[0] * b1[0] + b1[1] * b1[1]).sqrt();
let mag_b2 = (b2[0] * b2[0] + b2[1] * b2[1]).sqrt();
let g_max = (n_max as f64) * mag_b1.max(mag_b2);
let g_max_sq = g_max * g_max;
let mut gvecs = Vec::new();
for m in -n_max..=n_max {
for n in -n_max..=n_max {
let gx = m as f64 * b1[0] + n as f64 * b2[0];
let gy = m as f64 * b1[1] + n as f64 * b2[1];
if gx * gx + gy * gy <= g_max_sq + 1e-10 {
gvecs.push([gx, gy]);
}
}
}
gvecs
}
pub fn eta_fourier_circular(
eps_incl: f64,
eps_bg: f64,
fill: f64,
cell_area: f64,
g_diff: [f64; 2],
) -> f64 {
let inv_incl = 1.0 / eps_incl;
let inv_bg = 1.0 / eps_bg;
let g_mag = (g_diff[0] * g_diff[0] + g_diff[1] * g_diff[1]).sqrt();
let g_red = g_mag / (2.0 * PI);
if g_red < 1e-12 {
inv_incl * fill + inv_bg * (1.0 - fill)
} else {
let r_tilde = (fill * cell_area / PI).sqrt();
let arg = 2.0 * PI * g_red * r_tilde;
let j1_over_arg = if arg < 1e-12 {
0.5
} else {
bessel_j1(arg) / arg
};
(inv_incl - inv_bg) * 2.0 * fill * j1_over_arg
}
}
pub fn tm_matrix(k: [f64; 2], gvecs: &[[f64; 2]], eta: &dyn Fn([f64; 2]) -> f64) -> Vec<f64> {
let n = gvecs.len();
let mut mat = vec![0.0_f64; n * n];
for i in 0..n {
let kgi_x = k[0] + gvecs[i][0];
let kgi_y = k[1] + gvecs[i][1];
let kgi_mag = (kgi_x * kgi_x + kgi_y * kgi_y).sqrt();
for j in 0..n {
let kgj_x = k[0] + gvecs[j][0];
let kgj_y = k[1] + gvecs[j][1];
let kgj_mag = (kgj_x * kgj_x + kgj_y * kgj_y).sqrt();
let g_diff = [gvecs[i][0] - gvecs[j][0], gvecs[i][1] - gvecs[j][1]];
mat[i * n + j] = kgi_mag * kgj_mag * eta(g_diff);
}
}
mat
}
pub fn te_matrix(k: [f64; 2], gvecs: &[[f64; 2]], eta: &dyn Fn([f64; 2]) -> f64) -> Vec<f64> {
let n = gvecs.len();
let mut mat = vec![0.0_f64; n * n];
for i in 0..n {
let kgi_x = k[0] + gvecs[i][0];
let kgi_y = k[1] + gvecs[i][1];
for j in 0..n {
let kgj_x = k[0] + gvecs[j][0];
let kgj_y = k[1] + gvecs[j][1];
let dot = kgi_x * kgj_x + kgi_y * kgj_y;
let g_diff = [gvecs[i][0] - gvecs[j][0], gvecs[i][1] - gvecs[j][1]];
mat[i * n + j] = dot * eta(g_diff);
}
}
mat
}
pub fn jacobi_eigh(a: &mut [f64], n: usize, tol: f64, max_sweeps: usize) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![a[0]];
}
for _sweep in 0..max_sweeps {
let mut max_off = 0.0_f64;
for p in 0..n {
for q in (p + 1)..n {
let v = a[p * n + q].abs();
if v > max_off {
max_off = v;
}
}
}
if max_off < tol {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq = a[p * n + q];
if apq.abs() < 1e-15 {
continue;
}
let app = a[p * n + p];
let aqq = a[q * n + q];
let theta = 0.5 * f64::atan2(2.0 * apq, app - aqq);
let c = theta.cos();
let s = theta.sin();
let new_app = c * c * app + 2.0 * c * s * apq + s * s * aqq;
let new_aqq = s * s * app - 2.0 * c * s * apq + c * c * aqq;
a[p * n + p] = new_app;
a[q * n + q] = new_aqq;
a[p * n + q] = 0.0;
a[q * n + p] = 0.0;
for r in 0..n {
if r == p || r == q {
continue;
}
let arp = a[r * n + p];
let arq = a[r * n + q];
let new_arp = c * arp + s * arq;
let new_arq = -s * arp + c * arq;
a[r * n + p] = new_arp;
a[p * n + r] = new_arp;
a[r * n + q] = new_arq;
a[q * n + r] = new_arq;
}
}
}
}
let mut eigs: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
eigs.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
eigs
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Polarization {
TE,
TM,
}
pub struct BandStructure {
pub k_path: Vec<[f64; 2]>,
pub bands: Vec<Vec<f64>>,
pub gaps: Vec<(f64, f64)>,
}
impl BandStructure {
pub fn find_gaps(&mut self) {
self.gaps.clear();
let nb = self.bands.len();
for i in 0..(nb.saturating_sub(1)) {
let lo = self.bands[i]
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let hi = self.bands[i + 1]
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
if hi > lo {
self.gaps.push((lo, hi));
}
}
}
}
pub struct PhCrystal2d {
pub a1: [f64; 2],
pub a2: [f64; 2],
pub eps_incl: f64,
pub eps_bg: f64,
pub fill: f64,
pub n_g: usize,
}
impl PhCrystal2d {
pub fn square_rods(eps_rod: f64, eps_bg: f64, fill: f64, n_g: usize) -> Self {
Self {
a1: [1.0, 0.0],
a2: [0.0, 1.0],
eps_incl: eps_rod,
eps_bg,
fill,
n_g,
}
}
pub fn hex_holes(eps_bg: f64, eps_hole: f64, fill: f64, n_g: usize) -> Self {
Self {
a1: [1.0, 0.0],
a2: [0.5, 3.0_f64.sqrt() / 2.0],
eps_incl: eps_hole,
eps_bg,
fill,
n_g,
}
}
fn cell_area(&self) -> f64 {
(self.a1[0] * self.a2[1] - self.a1[1] * self.a2[0]).abs()
}
fn build_gvecs(&self) -> Vec<[f64; 2]> {
let (b1, b2) = recip_lattice(self.a1, self.a2);
gen_gvecs(b1, b2, self.n_g as i32)
}
fn eta_fn(&self) -> impl Fn([f64; 2]) -> f64 + '_ {
let eps_incl = self.eps_incl;
let eps_bg = self.eps_bg;
let fill = self.fill;
let cell_area = self.cell_area();
move |g_diff: [f64; 2]| eta_fourier_circular(eps_incl, eps_bg, fill, cell_area, g_diff)
}
fn k_to_phys(k_red: [f64; 2]) -> [f64; 2] {
[k_red[0] * 2.0 * PI, k_red[1] * 2.0 * PI]
}
pub fn solve_tm(&self, k_red: [f64; 2]) -> Vec<f64> {
let gvecs = self.build_gvecs();
let eta = self.eta_fn();
let k_phys = Self::k_to_phys(k_red);
let mut mat = tm_matrix(k_phys, &gvecs, &eta);
let n = gvecs.len();
let raw = jacobi_eigh(&mut mat, n, 1e-10, 50);
raw.into_iter()
.map(|ev| {
let ev_norm = ev / (4.0 * PI * PI);
if ev_norm < 0.0 {
0.0
} else {
ev_norm.sqrt()
}
})
.collect()
}
pub fn solve_te(&self, k_red: [f64; 2]) -> Vec<f64> {
let gvecs = self.build_gvecs();
let eta = self.eta_fn();
let k_phys = Self::k_to_phys(k_red);
let mut mat = te_matrix(k_phys, &gvecs, &eta);
let n = gvecs.len();
let raw = jacobi_eigh(&mut mat, n, 1e-10, 50);
raw.into_iter()
.map(|ev| {
let ev_norm = ev / (4.0 * PI * PI);
if ev_norm < 0.0 {
0.0
} else {
ev_norm.sqrt()
}
})
.collect()
}
pub fn band_diagram(&self, k_path: &[[f64; 2]], pol: Polarization) -> BandStructure {
let gvecs = self.build_gvecs();
let n_total = gvecs.len();
let n_bands = n_total.min(10);
let eta = self.eta_fn();
let mut bands: Vec<Vec<f64>> = (0..n_bands).map(|_| Vec::new()).collect();
for &k_red in k_path {
let k_phys = Self::k_to_phys(k_red);
let mut mat = match pol {
Polarization::TM => tm_matrix(k_phys, &gvecs, &eta),
Polarization::TE => te_matrix(k_phys, &gvecs, &eta),
};
let raw = jacobi_eigh(&mut mat, n_total, 1e-10, 50);
for (band_idx, band) in bands.iter_mut().enumerate() {
let ev = raw[band_idx];
let ev_norm = ev / (4.0 * PI * PI);
let freq = if ev_norm < 0.0 { 0.0 } else { ev_norm.sqrt() };
band.push(freq);
}
}
let mut bs = BandStructure {
k_path: k_path.to_vec(),
bands,
gaps: Vec::new(),
};
bs.find_gaps();
bs
}
}
pub fn kpath_square(n_per_segment: usize) -> Vec<[f64; 2]> {
let gamma = [0.0_f64, 0.0];
let x_pt = [0.5, 0.0];
let m_pt = [0.5, 0.5];
let mut path = Vec::with_capacity(n_per_segment * 3 + 1);
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(gamma, x_pt, t));
}
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(x_pt, m_pt, t));
}
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(m_pt, gamma, t));
}
path.push(gamma);
path
}
pub fn kpath_hexagonal(n_per_segment: usize) -> Vec<[f64; 2]> {
let sqrt3 = 3.0_f64.sqrt();
let gamma = [0.0_f64, 0.0];
let m_pt = [0.5_f64, -0.5 / sqrt3];
let k_pt = [1.0 / 3.0, -1.0 / (3.0 * sqrt3) + 2.0 / (3.0 * sqrt3)];
let mut path = Vec::with_capacity(n_per_segment * 3 + 1);
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(gamma, m_pt, t));
}
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(m_pt, k_pt, t));
}
for i in 0..n_per_segment {
let t = i as f64 / n_per_segment as f64;
path.push(lerp(k_pt, gamma, t));
}
path.push(gamma);
path
}
#[inline]
fn lerp(a: [f64; 2], b: [f64; 2], t: f64) -> [f64; 2] {
[a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bessel_j1_at_zero() {
assert!((bessel_j1(0.0)).abs() < 1e-14);
}
#[test]
fn bessel_j1_at_one() {
let expected = 0.4400505857449335_f64;
let got = bessel_j1(1.0);
assert!(
(got - expected).abs() < 1e-8,
"J₁(1) = {} (expected {})",
got,
expected
);
}
#[test]
fn bessel_j1_first_zero() {
let val = bessel_j1(3.8317);
assert!(val.abs() < 1e-3, "J₁(3.8317) = {}", val);
}
#[test]
fn recip_lattice_square() {
let (b1, b2) = recip_lattice([1.0, 0.0], [0.0, 1.0]);
assert!((b1[0] - 2.0 * PI).abs() < 1e-10);
assert!(b1[1].abs() < 1e-10);
assert!(b2[0].abs() < 1e-10);
assert!((b2[1] - 2.0 * PI).abs() < 1e-10);
}
#[test]
fn jacobi_eigh_2x2() {
let mut a = vec![3.0_f64, 1.0, 1.0, 2.0];
let eigs = jacobi_eigh(&mut a, 2, 1e-12, 50);
let lam1 = (5.0 - 5.0_f64.sqrt()) / 2.0;
let lam2 = (5.0 + 5.0_f64.sqrt()) / 2.0;
assert!((eigs[0] - lam1).abs() < 1e-10, "eig[0]={}", eigs[0]);
assert!((eigs[1] - lam2).abs() < 1e-10, "eig[1]={}", eigs[1]);
}
#[test]
fn empty_lattice_lowest_tm_at_gamma() {
let crystal = PhCrystal2d::square_rods(1.0, 1.0, 0.3, 3);
let freqs = crystal.solve_tm([0.0, 0.0]);
assert!(freqs[0].abs() < 1e-8, "Lowest TM at Γ = {}", freqs[0]);
}
}