use crate::error::{self, Result};
use crate::math::Complex;
pub struct BogoliubovTransform {
pub u: Vec<f64>,
pub v: Vec<f64>,
pub omega: Vec<f64>,
a_k: Vec<f64>,
b_k: Vec<f64>,
}
impl BogoliubovTransform {
pub fn new(u: Vec<f64>, v: Vec<f64>, omega: Vec<f64>) -> Result<Self> {
if u.len() != v.len() || u.len() != omega.len() {
return Err(error::invalid_param(
"u/v/omega",
"u, v, and omega must have the same length",
));
}
const TOL: f64 = 1e-8;
for (i, (&ui, &vi)) in u.iter().zip(v.iter()).enumerate() {
let norm = ui * ui - vi * vi;
if (norm - 1.0).abs() > TOL {
return Err(error::invalid_param(
"u,v",
&format!("mode {i}: u²−v²={norm:.3e} deviates from 1 by more than {TOL:.0e}"),
));
}
}
let n = u.len();
let a_k = vec![0.0; n];
let b_k = vec![0.0; n];
Ok(Self {
u,
v,
omega,
a_k,
b_k,
})
}
pub fn from_hamiltonian(a_k: &[f64], b_k: &[f64]) -> Result<Self> {
if a_k.len() != b_k.len() {
return Err(error::invalid_param(
"a_k/b_k",
"A_k and B_k arrays must have the same length",
));
}
let n = a_k.len();
let mut u = Vec::with_capacity(n);
let mut v = Vec::with_capacity(n);
let mut omega = Vec::with_capacity(n);
for (i, (&ai, &bi)) in a_k.iter().zip(b_k.iter()).enumerate() {
if bi.abs() >= ai.abs() {
return Err(error::numerical_error(&format!(
"mode {i}: |B_k|={} >= |A_k|={} — system is magnetically unstable",
bi.abs(),
ai.abs()
)));
}
let omega_k = (ai * ai - bi * bi).sqrt();
let tanh_2theta = -bi / ai;
let two_theta = tanh_2theta.atanh();
let theta = two_theta / 2.0;
let uk = theta.cosh();
let vk = theta.sinh();
u.push(uk);
v.push(vk);
omega.push(omega_k);
}
Ok(Self {
u,
v,
omega,
a_k: a_k.to_vec(),
b_k: b_k.to_vec(),
})
}
pub fn from_afm_square_lattice(j: f64, h_ext: f64, n_k: usize) -> Result<Self> {
if j <= 0.0 {
return Err(error::invalid_param(
"j",
"exchange coupling must be positive",
));
}
if n_k < 3 {
return Err(error::invalid_param(
"n_k",
"need at least 3 k-points for path sampling",
));
}
let seg = n_k / 3;
let seg0 = seg;
let seg1 = seg;
let seg2 = n_k - 2 * seg;
let mut a_vals = Vec::with_capacity(n_k);
let mut b_vals = Vec::with_capacity(n_k);
for i in 0..seg0 {
let t = i as f64 / seg0 as f64;
let kx = t * std::f64::consts::PI;
let ky = 0.0_f64;
let gamma_k = (kx.cos() + ky.cos()) / 2.0;
let ai = 4.0 * j + h_ext;
let bi = 4.0 * j * gamma_k;
a_vals.push(ai);
b_vals.push(bi);
}
for i in 0..seg1 {
let t = i as f64 / seg1 as f64;
let kx = std::f64::consts::PI;
let ky = t * std::f64::consts::PI;
let gamma_k = (kx.cos() + ky.cos()) / 2.0;
let ai = 4.0 * j + h_ext;
let bi = 4.0 * j * gamma_k;
a_vals.push(ai);
b_vals.push(bi);
}
for i in 0..seg2 {
let t = i as f64 / (seg2 - 1).max(1) as f64;
let kx = (1.0 - t) * std::f64::consts::PI;
let ky = (1.0 - t) * std::f64::consts::PI;
let gamma_k = (kx.cos() + ky.cos()) / 2.0;
let ai = 4.0 * j + h_ext;
let bi = 4.0 * j * gamma_k;
a_vals.push(ai);
b_vals.push(bi);
}
Self::from_hamiltonian(&a_vals, &b_vals)
}
pub fn n_modes(&self) -> usize {
self.u.len()
}
#[inline]
pub fn magnon_frequency(&self, k_index: usize) -> Result<f64> {
if k_index >= self.omega.len() {
return Err(error::invalid_param(
"k_index",
&format!(
"index {k_index} out of range for {} modes",
self.omega.len()
),
));
}
Ok(self.omega[k_index])
}
pub fn squeeze_parameter(&self, k_index: usize) -> Result<f64> {
if k_index >= self.u.len() {
return Err(error::invalid_param(
"k_index",
&format!("index {k_index} out of range"),
));
}
let uk = self.u[k_index];
let vk = self.v[k_index];
Ok((vk / uk).atanh())
}
#[inline]
pub fn vacuum_occupation(&self, k_index: usize) -> Result<f64> {
if k_index >= self.v.len() {
return Err(error::invalid_param(
"k_index",
&format!("index {k_index} out of range"),
));
}
Ok(self.v[k_index] * self.v[k_index])
}
pub fn total_vacuum_magnons(&self) -> f64 {
self.v.iter().map(|&vk| vk * vk).sum()
}
pub fn check_normalization(&self, tol: f64) -> bool {
self.u
.iter()
.zip(self.v.iter())
.all(|(&uk, &vk)| (uk * uk - vk * vk - 1.0).abs() < tol)
}
pub fn apply_to_state(&self, input: &[Complex]) -> Result<Vec<Complex>> {
if input.len() != self.u.len() {
return Err(error::invalid_param(
"input",
&format!(
"state vector length {} does not match n_modes={}",
input.len(),
self.u.len()
),
));
}
let output: Vec<Complex> = input
.iter()
.zip(self.u.iter().zip(self.v.iter()))
.map(|(&c, (&uk, &vk))| {
let normal = c.scale(uk);
let anomal = c.conj().scale(vk);
normal.add(&anomal)
})
.collect();
Ok(output)
}
pub fn ground_state_energy(&self) -> f64 {
let sum: f64 = self
.omega
.iter()
.zip(self.a_k.iter())
.map(|(&ok, &ak)| ok - ak)
.sum();
0.5 * sum
}
pub fn pairing_coefficient(&self, k_index: usize) -> Result<f64> {
if k_index >= self.b_k.len() {
return Err(error::invalid_param(
"k_index",
&format!("index {k_index} out of range"),
));
}
Ok(self.b_k[k_index])
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
#[test]
fn test_normalization_u_sq_minus_v_sq_equals_one() {
let a = vec![10.0, 8.0, 6.0];
let b = vec![2.0, 1.5, 0.5];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
assert!(bogo.check_normalization(1e-12), "normalization must hold");
}
#[test]
fn test_zero_pairing_gives_identity_transformation() {
let a = vec![5.0, 3.0];
let b = vec![0.0, 0.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
for i in 0..2 {
assert!((bogo.u[i] - 1.0).abs() < TOL, "u should be 1 at B=0");
assert!(bogo.v[i].abs() < TOL, "v should be 0 at B=0");
}
assert!((bogo.omega[0] - 5.0).abs() < TOL);
assert!((bogo.omega[1] - 3.0).abs() < TOL);
}
#[test]
fn test_diverges_when_b_ge_a() {
let a = vec![3.0];
let b = vec![3.0]; assert!(BogoliubovTransform::from_hamiltonian(&a, &b).is_err());
let b2 = vec![4.0]; assert!(BogoliubovTransform::from_hamiltonian(&a, &b2).is_err());
}
#[test]
fn test_omega_positive_for_stable_modes() {
let a = vec![10.0, 5.0, 20.0];
let b = vec![2.0, 1.0, 4.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
for (i, &ok) in bogo.omega.iter().enumerate() {
assert!(ok > 0.0, "omega[{i}] = {ok} should be positive");
}
}
#[test]
fn test_squeeze_parameter_zero_at_no_pairing() {
let a = vec![7.0];
let b = vec![0.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
let theta = bogo.squeeze_parameter(0).expect("valid index");
assert!(theta.abs() < TOL, "theta should be 0 when B=0, got {theta}");
}
#[test]
fn test_vacuum_occupation_increases_with_b_over_a() {
let a = vec![10.0, 10.0];
let b_small = vec![1.0, 0.0];
let b_large = vec![8.0, 0.0];
let bogo_small = BogoliubovTransform::from_hamiltonian(&a, &b_small).expect("stable");
let bogo_large = BogoliubovTransform::from_hamiltonian(&a, &b_large).expect("stable");
let occ_small = bogo_small.vacuum_occupation(0).expect("valid");
let occ_large = bogo_large.vacuum_occupation(0).expect("valid");
assert!(occ_large > occ_small, "larger B/A should give larger v²");
}
#[test]
fn test_apply_to_state_preserves_norm_for_real_input() {
let a = vec![5.0];
let b = vec![0.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
let input = vec![Complex::new(0.6, 0.8)]; let output = bogo.apply_to_state(&input).expect("valid");
let norm_in = input[0].norm_sq();
let norm_out = output[0].norm_sq();
assert!(
(norm_in - norm_out).abs() < TOL,
"norm must be preserved for B=0"
);
}
#[test]
fn test_afm_square_lattice_dispersion_at_gamma_point() {
let j = 1.0;
let h_ext = 0.5;
let bogo = BogoliubovTransform::from_afm_square_lattice(j, h_ext, 30).expect("stable");
let omega_gamma = bogo.magnon_frequency(0).expect("valid");
let expected = (h_ext * (8.0 * j + h_ext)).sqrt();
let rel_err = (omega_gamma - expected).abs() / expected;
assert!(
rel_err < 1e-8,
"Γ-point omega mismatch: got {omega_gamma}, expected {expected}"
);
}
#[test]
fn test_afm_zone_boundary_at_x_point() {
let j = 2.0;
let h_ext = 0.1;
let bogo = BogoliubovTransform::from_afm_square_lattice(j, h_ext, 3).expect("stable");
let omega_x = bogo.magnon_frequency(1).expect("valid");
let expected = 4.0 * j + h_ext;
let rel_err = (omega_x - expected).abs() / expected;
assert!(
rel_err < 1e-10,
"X-point omega mismatch: got {omega_x}, expected {expected}"
);
}
#[test]
fn test_total_vacuum_magnons_positive() {
let a = vec![5.0, 8.0, 3.0];
let b = vec![1.0, 2.0, 0.5];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
assert!(bogo.total_vacuum_magnons() > 0.0);
}
#[test]
fn test_ground_state_energy_negative() {
let a = vec![10.0, 8.0];
let b = vec![3.0, 2.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
let e0 = bogo.ground_state_energy();
assert!(
e0 < 0.0,
"ground state energy shift should be negative when B≠0, got {e0}"
);
}
#[test]
fn test_check_normalization_tol() {
let a = vec![6.0];
let b = vec![2.0];
let bogo = BogoliubovTransform::from_hamiltonian(&a, &b).expect("stable");
assert!(bogo.check_normalization(1e-10));
assert!(bogo.check_normalization(1e-12));
}
#[test]
fn test_new_validates_lengths_match() {
let u = vec![1.0, 1.0];
let v = vec![0.0];
let omega = vec![5.0, 3.0];
assert!(BogoliubovTransform::new(u, v, omega).is_err());
}
#[test]
fn test_new_rejects_unnormalized() {
let u = vec![2.0];
let v = vec![0.0];
let omega = vec![5.0];
assert!(BogoliubovTransform::new(u, v, omega).is_err());
}
}