use crate::constants::{HBAR, KB};
use crate::error::{self, Result};
use crate::spinwave::QuantizedModes;
pub struct ZeroPointFluctuations {
pub modes: Vec<(usize, f64)>,
pub m_eff: f64,
pub spin: f64,
pub volume: f64,
}
impl ZeroPointFluctuations {
pub fn new(modes: Vec<(usize, f64)>, m_eff: f64, spin: f64, volume: f64) -> Result<Self> {
if m_eff <= 0.0 {
return Err(error::invalid_param(
"m_eff",
"effective mass must be positive",
));
}
if volume <= 0.0 {
return Err(error::invalid_param(
"volume",
"mode volume must be positive",
));
}
if spin <= 0.0 {
return Err(error::invalid_param(
"spin",
"spin quantum number must be positive",
));
}
for (idx, omega) in &modes {
if *omega <= 0.0 {
return Err(error::invalid_param(
"omega",
&format!("mode {idx}: frequency must be positive, got {omega}"),
));
}
}
Ok(Self {
modes,
m_eff,
spin,
volume,
})
}
pub fn from_quantized_modes(
quantized_modes: &QuantizedModes,
h_ext: f64,
n_modes: usize,
spin: f64,
m_eff: f64,
volume: f64,
) -> Result<Self> {
let freq_pairs = quantized_modes.stripe_mode_frequencies(h_ext, n_modes)?;
let modes: Vec<(usize, f64)> = freq_pairs.into_iter().collect();
Self::new(modes, m_eff, spin, volume)
}
pub fn n_modes(&self) -> usize {
self.modes.len()
}
pub fn zero_point_amplitude(&self, mode_idx: usize) -> Result<f64> {
if mode_idx >= self.modes.len() {
return Err(error::invalid_param(
"mode_idx",
&format!(
"index {mode_idx} out of range for {} modes",
self.modes.len()
),
));
}
let (_n, omega) = self.modes[mode_idx];
Ok((HBAR / (2.0 * self.m_eff * omega)).sqrt())
}
pub fn ground_state_energy(&self) -> f64 {
let omega_sum: f64 = self.modes.iter().map(|&(_, omega)| omega).sum();
0.5 * HBAR * omega_sum
}
pub fn vacuum_fluctuation_sx(&self) -> Result<f64> {
let mut sum = 0.0;
for idx in 0..self.modes.len() {
let un = self.zero_point_amplitude(idx)?;
sum += un * un;
}
Ok(sum / self.volume)
}
pub fn casimir_free_energy(&self, temperature: f64) -> Result<f64> {
if temperature < 0.0 {
return Err(error::invalid_param(
"temperature",
"temperature must be non-negative",
));
}
let mut f = 0.0;
for &(_, omega) in &self.modes {
if temperature < 1e-30 {
f += 0.5 * HBAR * omega;
} else {
let x = HBAR * omega / (KB * temperature);
let n_be = if x > 300.0 {
(-x).exp()
} else {
1.0 / (x.exp() - 1.0)
};
f += HBAR * omega * (0.5 + n_be);
}
}
Ok(f)
}
pub fn casimir_pressure_change(&self, length: f64, dl: f64) -> Result<f64> {
if length <= 0.0 {
return Err(error::invalid_param(
"length",
"confinement length must be positive",
));
}
if dl <= 0.0 {
return Err(error::invalid_param(
"dl",
"length increment dl must be positive",
));
}
let f0 = self.ground_state_energy();
let scale = length / (length + dl);
let shifted_modes: Vec<(usize, f64)> = self
.modes
.iter()
.map(|&(n, omega)| (n, omega * scale))
.collect();
let f1: f64 = shifted_modes
.iter()
.map(|&(_, omega)| 0.5 * HBAR * omega)
.sum();
Ok((f1 - f0) / dl)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ME_EFF: f64 = 9.109e-31; const VOL: f64 = 1e-21;
fn make_zpf(n: usize) -> ZeroPointFluctuations {
let omega0 = 1e11; let modes: Vec<(usize, f64)> = (1..=n).map(|i| (i, i as f64 * omega0)).collect();
ZeroPointFluctuations::new(modes, ME_EFF, 1.0, VOL).expect("valid params")
}
#[test]
fn test_amplitude_positive() {
let zpf = make_zpf(3);
for i in 0..3 {
let u = zpf.zero_point_amplitude(i).expect("valid index");
assert!(u > 0.0, "amplitude at mode {i} should be positive, got {u}");
}
}
#[test]
fn test_amplitude_scales_as_inverse_sqrt_omega() {
let zpf = make_zpf(2);
let u0 = zpf.zero_point_amplitude(0).expect("valid");
let u1 = zpf.zero_point_amplitude(1).expect("valid");
let ratio = u0 / u1;
let expected = 2.0_f64.sqrt();
assert!(
(ratio - expected).abs() < 1e-10,
"u_0/u_1 should be sqrt(2), got {ratio}"
);
}
#[test]
fn test_ground_state_energy_positive() {
let zpf = make_zpf(5);
let e0 = zpf.ground_state_energy();
assert!(e0 > 0.0, "ground state energy must be positive, got {e0}");
}
#[test]
fn test_vacuum_fluctuation_positive() {
let zpf = make_zpf(4);
let sx = zpf.vacuum_fluctuation_sx().expect("valid");
assert!(sx > 0.0, "vacuum fluctuation Sx must be positive, got {sx}");
}
#[test]
fn test_casimir_at_zero_t_equals_ground_state() {
let zpf = make_zpf(3);
let e0 = zpf.ground_state_energy();
let f_low = zpf.casimir_free_energy(1e-50).expect("valid");
let rel_err = (f_low - e0).abs() / e0;
assert!(
rel_err < 1e-6,
"Casimir F(T→0) should equal E_0, rel err={rel_err}"
);
}
#[test]
fn test_casimir_high_t_classical_limit() {
let zpf = make_zpf(3);
let t = 1e6; let f_high = zpf.casimir_free_energy(t).expect("valid");
let n_modes = zpf.n_modes() as f64;
let classical = n_modes * KB * t;
let ratio = f_high / classical;
assert!(
(ratio - 1.0).abs() < 0.05,
"high-T limit: F/(NkT) = {ratio}, expected ≈1"
);
}
#[test]
fn test_from_quantized_modes_round_trip_n_modes() {
use crate::material::Ferromagnet;
use crate::spinwave::{NanostructureGeometry, QuantizedModes};
let py = Ferromagnet::permalloy();
let geo = NanostructureGeometry::Stripe {
width: 1e-6,
length: 10e-6,
};
let qm = QuantizedModes::new(&py, geo, 50e-9).expect("valid");
let n_req = 5;
let zpf = ZeroPointFluctuations::from_quantized_modes(&qm, 0.05, n_req, 1.0, ME_EFF, VOL)
.expect("valid zpf");
assert_eq!(zpf.n_modes(), n_req, "round-trip must preserve mode count");
}
#[test]
fn test_negative_m_eff_rejected() {
let modes = vec![(1, 1e11)];
assert!(ZeroPointFluctuations::new(modes, -1.0, 1.0, VOL).is_err());
}
#[test]
fn test_negative_volume_rejected() {
let modes = vec![(1, 1e11)];
assert!(ZeroPointFluctuations::new(modes, ME_EFF, 1.0, -1.0).is_err());
}
#[test]
fn test_invalid_mode_idx() {
let zpf = make_zpf(2);
assert!(zpf.zero_point_amplitude(5).is_err());
}
#[test]
fn test_casimir_free_energy_increases_with_t() {
let zpf = make_zpf(3);
let f_low = zpf.casimir_free_energy(1.0).expect("valid");
let f_mid = zpf.casimir_free_energy(100.0).expect("valid");
let f_high = zpf.casimir_free_energy(10000.0).expect("valid");
assert!(f_mid > f_low, "F should increase with T");
assert!(f_high > f_mid, "F should increase with T");
}
}