use std::f64::consts::PI;
use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::vector3::Vector3;
pub struct HopfFibration;
impl HopfFibration {
pub fn map(a: f64, b: f64, c: f64, d: f64) -> Vector3<f64> {
let norm_sq = a * a + b * b + c * c + d * d;
let inv_norm_sq = if norm_sq > 1e-30 { 1.0 / norm_sq } else { 1.0 };
Vector3::new(
2.0 * (a * c + b * d) * inv_norm_sq,
2.0 * (b * c - a * d) * inv_norm_sq,
(a * a + b * b - c * c - d * d) * inv_norm_sq,
)
}
pub fn inverse_map(n: &Vector3<f64>, fiber_angle: f64) -> (f64, f64, f64, f64) {
let mag = (n.x * n.x + n.y * n.y + n.z * n.z).sqrt();
let nx = if mag > 1e-30 { n.x / mag } else { 0.0 };
let ny = if mag > 1e-30 { n.y / mag } else { 0.0 };
let nz = if mag > 1e-30 { n.z / mag } else { 1.0 };
let nz_clamped = nz.clamp(-1.0, 1.0);
let abs_z1 = ((1.0 + nz_clamped) / 2.0).sqrt();
let abs_z2 = ((1.0 - nz_clamped) / 2.0).sqrt();
let phase_product = ny.atan2(nx); let arg_z1 = fiber_angle;
let arg_z2 = fiber_angle - phase_product;
let a = abs_z1 * arg_z1.cos();
let b = abs_z1 * arg_z1.sin();
let c = abs_z2 * arg_z2.cos();
let d = abs_z2 * arg_z2.sin();
(a, b, c, d)
}
pub fn magnetization_from_position(x: f64, y: f64, z: f64, radius: f64) -> Vector3<f64> {
let safe_radius = if radius.abs() < 1e-30 { 1e-30 } else { radius };
let xn = x / safe_radius;
let yn = y / safe_radius;
let zn = z / safe_radius;
let r_sq = xn * xn + yn * yn + zn * zn;
let denom = r_sq + 1.0;
let inv_denom = 1.0 / denom;
let s3_a = 2.0 * xn * inv_denom;
let s3_b = 2.0 * yn * inv_denom;
let s3_c = 2.0 * zn * inv_denom;
let s3_d = (r_sq - 1.0) * inv_denom;
Self::map(s3_a, s3_b, s3_c, s3_d)
}
}
pub struct ToroidalCoords;
impl ToroidalCoords {
pub fn from_cartesian(x: f64, y: f64, z: f64, major_radius: f64) -> (f64, f64, f64) {
let phi = y.atan2(x);
let r_cyl = (x * x + y * y).sqrt();
let dr = r_cyl - major_radius;
let rho = (dr * dr + z * z).sqrt();
let theta = z.atan2(dr);
(rho, theta, phi)
}
pub fn to_cartesian(rho: f64, theta: f64, phi: f64, major_radius: f64) -> (f64, f64, f64) {
let r_cyl = major_radius + rho * theta.cos();
let x = r_cyl * phi.cos();
let y = r_cyl * phi.sin();
let z = rho * theta.sin();
(x, y, z)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ProfileFunction {
#[default]
Linear,
Tanh {
wall_width: f64,
},
Gaussian {
sigma: f64,
},
}
impl ProfileFunction {
pub fn evaluate(&self, rho: f64, radius: f64) -> f64 {
let safe_radius = if radius.abs() < 1e-30 { 1e-30 } else { radius };
match self {
ProfileFunction::Linear => {
if rho < safe_radius {
PI * (1.0 - rho / safe_radius)
} else {
0.0
}
},
ProfileFunction::Tanh { wall_width } => {
let safe_w = if wall_width.abs() < 1e-30 {
1e-30
} else {
*wall_width
};
PI * (1.0 - (rho / safe_w).tanh()) / 2.0
},
ProfileFunction::Gaussian { sigma } => {
let safe_sigma = if sigma.abs() < 1e-30 { 1e-30 } else { *sigma };
PI * (-rho * rho / (2.0 * safe_sigma * safe_sigma)).exp()
},
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Hopfion {
pub grid_size: (usize, usize, usize),
pub magnetization: Vec<Vector3<f64>>,
pub hopf_charge: i32,
pub radius: f64,
pub cell_size: f64,
pub profile: ProfileFunction,
pub azimuthal_winding: i32,
pub poloidal_winding: i32,
}
impl Hopfion {
pub fn new(grid_size: (usize, usize, usize), radius: f64, hopf_charge: i32) -> Result<Self> {
Self::with_profile(grid_size, radius, hopf_charge, ProfileFunction::Linear)
}
pub fn with_profile(
grid_size: (usize, usize, usize),
radius: f64,
hopf_charge: i32,
profile: ProfileFunction,
) -> Result<Self> {
let (nx, ny, nz) = grid_size;
if nx == 0 || ny == 0 || nz == 0 {
return Err(Error::InvalidParameter {
param: "grid_size".to_string(),
reason: "All grid dimensions must be positive".to_string(),
});
}
if radius <= 0.0 || !radius.is_finite() {
return Err(Error::InvalidParameter {
param: "radius".to_string(),
reason: "Radius must be positive and finite".to_string(),
});
}
let min_dim = nx.min(ny).min(nz) as f64;
let cell_size = 4.0 * radius / min_dim;
let azimuthal_winding = hopf_charge;
let poloidal_winding = 1;
let total_size = nx * ny * nz;
let mut magnetization = Vec::with_capacity(total_size);
let cx = (nx as f64 - 1.0) / 2.0;
let cy = (ny as f64 - 1.0) / 2.0;
let cz = (nz as f64 - 1.0) / 2.0;
let major_radius = radius;
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let x = (ix as f64 - cx) * cell_size;
let y = (iy as f64 - cy) * cell_size;
let z = (iz as f64 - cz) * cell_size;
let m = Self::compute_magnetization_at_point(
x,
y,
z,
major_radius,
radius,
azimuthal_winding,
poloidal_winding,
&profile,
);
magnetization.push(m);
}
}
}
Ok(Self {
grid_size,
magnetization,
hopf_charge,
radius,
cell_size,
profile,
azimuthal_winding,
poloidal_winding,
})
}
fn compute_magnetization_at_point(
x: f64,
y: f64,
z: f64,
major_radius: f64,
profile_radius: f64,
n_winding: i32,
q_winding: i32,
profile: &ProfileFunction,
) -> Vector3<f64> {
let (rho, theta, phi) = ToroidalCoords::from_cartesian(x, y, z, major_radius);
let f_rho = profile.evaluate(rho, profile_radius);
let phase = n_winding as f64 * phi + q_winding as f64 * theta;
let sin_f = f_rho.sin();
let cos_f = f_rho.cos();
let mx = sin_f * phase.cos();
let my = sin_f * phase.sin();
let mz = cos_f;
let m = Vector3::new(mx, my, mz);
let mag = m.magnitude();
if mag > 1e-30 {
Vector3::new(mx / mag, my / mag, mz / mag)
} else {
Vector3::new(0.0, 0.0, 1.0)
}
}
pub fn linear_index(&self, ix: usize, iy: usize, iz: usize) -> Option<usize> {
let (nx, ny, nz) = self.grid_size;
if ix < nx && iy < ny && iz < nz {
Some(ix + iy * nx + iz * nx * ny)
} else {
None
}
}
pub fn magnetization_at(&self, ix: usize, iy: usize, iz: usize) -> Vector3<f64> {
match self.linear_index(ix, iy, iz) {
Some(idx) if idx < self.magnetization.len() => self.magnetization[idx],
_ => Vector3::new(0.0, 0.0, 1.0),
}
}
pub fn set_magnetization_at(
&mut self,
ix: usize,
iy: usize,
iz: usize,
m: Vector3<f64>,
) -> Result<()> {
let idx = self
.linear_index(ix, iy, iz)
.ok_or_else(|| Error::InvalidParameter {
param: "grid_index".to_string(),
reason: format!(
"Index ({}, {}, {}) out of bounds for grid {:?}",
ix, iy, iz, self.grid_size
),
})?;
if idx >= self.magnetization.len() {
return Err(Error::InvalidParameter {
param: "grid_index".to_string(),
reason: "Linear index exceeds magnetization array length".to_string(),
});
}
let mag = m.magnitude();
self.magnetization[idx] = if mag > 1e-30 {
Vector3::new(m.x / mag, m.y / mag, m.z / mag)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
Ok(())
}
pub fn is_normalized(&self, tolerance: f64) -> bool {
self.magnetization
.iter()
.all(|m| (m.magnitude() - 1.0).abs() < tolerance)
}
pub fn normalize_all(&mut self) {
for m in &mut self.magnetization {
let mag = m.magnitude();
if mag > 1e-30 {
m.x /= mag;
m.y /= mag;
m.z /= mag;
} else {
*m = Vector3::new(0.0, 0.0, 1.0);
}
}
}
pub fn grid_position(&self, ix: usize, iy: usize, iz: usize) -> (f64, f64, f64) {
let (nx, ny, nz) = self.grid_size;
let cx = (nx as f64 - 1.0) / 2.0;
let cy = (ny as f64 - 1.0) / 2.0;
let cz = (nz as f64 - 1.0) / 2.0;
let x = (ix as f64 - cx) * self.cell_size;
let y = (iy as f64 - cy) * self.cell_size;
let z = (iz as f64 - cz) * self.cell_size;
(x, y, z)
}
pub fn uniform(grid_size: (usize, usize, usize), cell_size: f64) -> Result<Self> {
let (nx, ny, nz) = grid_size;
if nx == 0 || ny == 0 || nz == 0 {
return Err(Error::InvalidParameter {
param: "grid_size".to_string(),
reason: "All grid dimensions must be positive".to_string(),
});
}
let total = nx * ny * nz;
let magnetization = vec![Vector3::new(0.0, 0.0, 1.0); total];
Ok(Self {
grid_size,
magnetization,
hopf_charge: 0,
radius: 0.0,
cell_size,
profile: ProfileFunction::Linear,
azimuthal_winding: 0,
poloidal_winding: 0,
})
}
}
impl fmt::Display for Hopfion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Hopfion[Q_H={}, R={:.1} nm, grid={}x{}x{}, N={}, Q={}]",
self.hopf_charge,
self.radius * 1e9,
self.grid_size.0,
self.grid_size.1,
self.grid_size.2,
self.azimuthal_winding,
self.poloidal_winding,
)
}
}
pub struct HopfInvariant;
impl HopfInvariant {
pub fn calculate(hopfion: &Hopfion) -> Result<f64> {
let (nx, ny, nz) = hopfion.grid_size;
if nx < 4 || ny < 4 || nz < 4 {
return Err(Error::InvalidParameter {
param: "grid_size".to_string(),
reason: "Grid must be at least 4x4x4 for Hopf invariant calculation".to_string(),
});
}
let dx = hopfion.cell_size;
let dv = dx * dx * dx;
let mut total = 0.0;
for iz in 1..nz - 1 {
for iy in 1..ny - 1 {
for ix in 1..nx - 1 {
let m = hopfion.magnetization_at(ix, iy, iz);
let dm_dx = (hopfion.magnetization_at(ix + 1, iy, iz)
- hopfion.magnetization_at(ix.saturating_sub(1), iy, iz))
* (0.5 / dx);
let dm_dy = (hopfion.magnetization_at(ix, iy + 1, iz)
- hopfion.magnetization_at(ix, iy.saturating_sub(1), iz))
* (0.5 / dx);
let dm_dz = (hopfion.magnetization_at(ix, iy, iz + 1)
- hopfion.magnetization_at(ix, iy, iz.saturating_sub(1)))
* (0.5 / dx);
let a_x = m.dot(&dm_dy.cross(&dm_dz));
let a_y = m.dot(&dm_dz.cross(&dm_dx));
let a_z = m.dot(&dm_dx.cross(&dm_dy));
let berry_a = Vector3::new(a_x, a_y, a_z);
let a_xp = Self::berry_connection_at(hopfion, ix + 1, iy, iz, dx);
let a_xm = Self::berry_connection_at(hopfion, ix.saturating_sub(1), iy, iz, dx);
let a_yp = Self::berry_connection_at(hopfion, ix, iy + 1, iz, dx);
let a_ym = Self::berry_connection_at(hopfion, ix, iy.saturating_sub(1), iz, dx);
let a_zp = Self::berry_connection_at(hopfion, ix, iy, iz + 1, dx);
let a_zm = Self::berry_connection_at(hopfion, ix, iy, iz.saturating_sub(1), dx);
let curl_a = Vector3::new(
(a_yp.z - a_ym.z - a_zp.y + a_zm.y) * (0.5 / dx),
(a_zp.x - a_zm.x - a_xp.z + a_xm.z) * (0.5 / dx),
(a_xp.y - a_xm.y - a_yp.x + a_ym.x) * (0.5 / dx),
);
total += berry_a.dot(&curl_a) * dv;
}
}
}
Ok(total / (4.0 * PI * PI))
}
fn berry_connection_at(
hopfion: &Hopfion,
ix: usize,
iy: usize,
iz: usize,
dx: f64,
) -> Vector3<f64> {
let (nx, ny, nz) = hopfion.grid_size;
let ix = ix.min(nx - 1);
let iy = iy.min(ny - 1);
let iz = iz.min(nz - 1);
let ix_lo = if ix > 0 { ix - 1 } else { ix };
let ix_hi = if ix + 1 < nx { ix + 1 } else { ix };
let iy_lo = if iy > 0 { iy - 1 } else { iy };
let iy_hi = if iy + 1 < ny { iy + 1 } else { iy };
let iz_lo = if iz > 0 { iz - 1 } else { iz };
let iz_hi = if iz + 1 < nz { iz + 1 } else { iz };
let m = hopfion.magnetization_at(ix, iy, iz);
let dx_scale = if ix_hi > ix_lo {
(ix_hi - ix_lo) as f64 * dx
} else {
dx
};
let dy_scale = if iy_hi > iy_lo {
(iy_hi - iy_lo) as f64 * dx
} else {
dx
};
let dz_scale = if iz_hi > iz_lo {
(iz_hi - iz_lo) as f64 * dx
} else {
dx
};
let dm_dx = (hopfion.magnetization_at(ix_hi, iy, iz)
- hopfion.magnetization_at(ix_lo, iy, iz))
* (1.0 / dx_scale);
let dm_dy = (hopfion.magnetization_at(ix, iy_hi, iz)
- hopfion.magnetization_at(ix, iy_lo, iz))
* (1.0 / dy_scale);
let dm_dz = (hopfion.magnetization_at(ix, iy, iz_hi)
- hopfion.magnetization_at(ix, iy, iz_lo))
* (1.0 / dz_scale);
Vector3::new(
m.dot(&dm_dy.cross(&dm_dz)),
m.dot(&dm_dz.cross(&dm_dx)),
m.dot(&dm_dx.cross(&dm_dy)),
)
}
pub fn calculate_uniform(grid_size: (usize, usize, usize), cell_size: f64) -> Result<f64> {
let uniform = Hopfion::uniform(grid_size, cell_size)?;
Self::calculate(&uniform)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HopfionEnergyParams {
pub exchange_stiffness: f64,
pub dmi_constant: f64,
pub external_field: Vector3<f64>,
pub saturation_magnetization: f64,
pub frustrated_exchange_j2: f64,
}
impl Default for HopfionEnergyParams {
fn default() -> Self {
Self {
exchange_stiffness: 1.0e-11, dmi_constant: 3.0e-3, external_field: Vector3::new(0.0, 0.0, 0.5), saturation_magnetization: 5.8e5, frustrated_exchange_j2: -2.0e-12, }
}
}
impl HopfionEnergyParams {
pub fn validate(&self) -> Result<()> {
if self.exchange_stiffness < 0.0 || !self.exchange_stiffness.is_finite() {
return Err(Error::InvalidParameter {
param: "exchange_stiffness".to_string(),
reason: "Must be non-negative and finite".to_string(),
});
}
if self.saturation_magnetization < 0.0 || !self.saturation_magnetization.is_finite() {
return Err(Error::InvalidParameter {
param: "saturation_magnetization".to_string(),
reason: "Must be non-negative and finite".to_string(),
});
}
if !self.dmi_constant.is_finite() {
return Err(Error::InvalidParameter {
param: "dmi_constant".to_string(),
reason: "Must be finite".to_string(),
});
}
Ok(())
}
}
pub struct HopfionEnergy;
impl HopfionEnergy {
pub fn exchange_energy(hopfion: &Hopfion, exchange_stiffness: f64) -> Result<f64> {
if !exchange_stiffness.is_finite() {
return Err(Error::InvalidParameter {
param: "exchange_stiffness".to_string(),
reason: "Must be finite".to_string(),
});
}
let (nx, ny, nz) = hopfion.grid_size;
let dx = hopfion.cell_size;
let dv = dx * dx * dx;
let inv_2dx = 0.5 / dx;
let mut energy = 0.0;
for iz in 1..nz.saturating_sub(1) {
for iy in 1..ny.saturating_sub(1) {
for ix in 1..nx.saturating_sub(1) {
let dm_dx = (hopfion.magnetization_at(ix + 1, iy, iz)
- hopfion.magnetization_at(ix - 1, iy, iz))
* inv_2dx;
let dm_dy = (hopfion.magnetization_at(ix, iy + 1, iz)
- hopfion.magnetization_at(ix, iy - 1, iz))
* inv_2dx;
let dm_dz = (hopfion.magnetization_at(ix, iy, iz + 1)
- hopfion.magnetization_at(ix, iy, iz - 1))
* inv_2dx;
let grad_sq = dm_dx.magnitude_squared()
+ dm_dy.magnitude_squared()
+ dm_dz.magnitude_squared();
energy += grad_sq * dv;
}
}
}
Ok(exchange_stiffness * energy)
}
pub fn dmi_energy(hopfion: &Hopfion, dmi_constant: f64) -> Result<f64> {
if !dmi_constant.is_finite() {
return Err(Error::InvalidParameter {
param: "dmi_constant".to_string(),
reason: "Must be finite".to_string(),
});
}
let (nx, ny, nz) = hopfion.grid_size;
let dx = hopfion.cell_size;
let dv = dx * dx * dx;
let inv_2dx = 0.5 / dx;
let mut energy = 0.0;
for iz in 1..nz.saturating_sub(1) {
for iy in 1..ny.saturating_sub(1) {
for ix in 1..nx.saturating_sub(1) {
let m = hopfion.magnetization_at(ix, iy, iz);
let dm_dx = (hopfion.magnetization_at(ix + 1, iy, iz)
- hopfion.magnetization_at(ix - 1, iy, iz))
* inv_2dx;
let dm_dy = (hopfion.magnetization_at(ix, iy + 1, iz)
- hopfion.magnetization_at(ix, iy - 1, iz))
* inv_2dx;
let dm_dz = (hopfion.magnetization_at(ix, iy, iz + 1)
- hopfion.magnetization_at(ix, iy, iz - 1))
* inv_2dx;
let curl_m =
Vector3::new(dm_dy.z - dm_dz.y, dm_dz.x - dm_dx.z, dm_dx.y - dm_dy.x);
energy += m.dot(&curl_m) * dv;
}
}
}
Ok(dmi_constant * energy)
}
pub fn zeeman_energy(
hopfion: &Hopfion,
field: &Vector3<f64>,
saturation_magnetization: f64,
) -> Result<f64> {
if !saturation_magnetization.is_finite() {
return Err(Error::InvalidParameter {
param: "saturation_magnetization".to_string(),
reason: "Must be finite".to_string(),
});
}
let dx = hopfion.cell_size;
let dv = dx * dx * dx;
let mut energy = 0.0;
for m in &hopfion.magnetization {
energy += m.dot(field);
}
Ok(-crate::constants::MU_0 * saturation_magnetization * energy * dv)
}
pub fn frustrated_exchange_energy(hopfion: &Hopfion, j2: f64) -> Result<f64> {
if !j2.is_finite() {
return Err(Error::InvalidParameter {
param: "j2".to_string(),
reason: "Must be finite".to_string(),
});
}
let (nx, ny, nz) = hopfion.grid_size;
let dx = hopfion.cell_size;
let dv = dx * dx * dx;
let inv_dx2 = 1.0 / (dx * dx);
let mut energy = 0.0;
for iz in 1..nz.saturating_sub(1) {
for iy in 1..ny.saturating_sub(1) {
for ix in 1..nx.saturating_sub(1) {
let m_c = hopfion.magnetization_at(ix, iy, iz);
let lap_x = (hopfion.magnetization_at(ix + 1, iy, iz)
+ hopfion.magnetization_at(ix - 1, iy, iz)
- m_c * 2.0)
* inv_dx2;
let lap_y = (hopfion.magnetization_at(ix, iy + 1, iz)
+ hopfion.magnetization_at(ix, iy - 1, iz)
- m_c * 2.0)
* inv_dx2;
let lap_z = (hopfion.magnetization_at(ix, iy, iz + 1)
+ hopfion.magnetization_at(ix, iy, iz - 1)
- m_c * 2.0)
* inv_dx2;
let laplacian = lap_x + lap_y + lap_z;
energy += laplacian.magnitude_squared() * dv;
}
}
}
Ok(j2 * energy)
}
pub fn total_energy(hopfion: &Hopfion, params: &HopfionEnergyParams) -> Result<f64> {
params.validate()?;
let e_ex = Self::exchange_energy(hopfion, params.exchange_stiffness)?;
let e_dmi = Self::dmi_energy(hopfion, params.dmi_constant)?;
let e_zee = Self::zeeman_energy(
hopfion,
¶ms.external_field,
params.saturation_magnetization,
)?;
let e_frust = Self::frustrated_exchange_energy(hopfion, params.frustrated_exchange_j2)?;
Ok(e_ex + e_dmi + e_zee + e_frust)
}
}
pub struct HopfionStability;
impl HopfionStability {
pub fn energy_vs_radius(
grid_size: (usize, usize, usize),
radii: &[f64],
hopf_charge: i32,
params: &HopfionEnergyParams,
) -> Result<Vec<(f64, f64)>> {
params.validate()?;
let mut results = Vec::with_capacity(radii.len());
for &r in radii {
if r <= 0.0 || !r.is_finite() {
continue;
}
let hopfion = Hopfion::new(grid_size, r, hopf_charge)?;
let energy = HopfionEnergy::total_energy(&hopfion, params)?;
results.push((r, energy));
}
Ok(results)
}
pub fn find_stability_boundaries(energy_landscape: &[(f64, f64)]) -> Option<(f64, f64, f64)> {
if energy_landscape.len() < 3 {
return None;
}
let mut min_idx = None;
let mut min_energy = f64::MAX;
for i in 1..energy_landscape.len() - 1 {
let (_, e_prev) = energy_landscape[i - 1];
let (_, e_curr) = energy_landscape[i];
let (_, e_next) = energy_landscape[i + 1];
if e_curr < e_prev && e_curr < e_next && e_curr < min_energy {
min_energy = e_curr;
min_idx = Some(i);
}
}
let eq_idx = min_idx?;
let (r_eq, _) = energy_landscape[eq_idx];
let mut r_min = energy_landscape[0].0;
for i in (0..eq_idx).rev() {
let (r_i, e_i) = energy_landscape[i];
let (_, e_next) = energy_landscape[i + 1];
if e_i < e_next {
r_min = r_i;
break;
}
}
let mut r_max = energy_landscape[energy_landscape.len() - 1].0;
for i in eq_idx + 1..energy_landscape.len() {
let (r_i, e_i) = energy_landscape[i];
let (_, e_prev) = energy_landscape[i - 1];
if e_i < e_prev {
r_max = r_i;
break;
}
}
Some((r_min, r_eq, r_max))
}
pub fn frustration_assessment(j1: f64, j2: f64) -> (f64, bool) {
if j1.abs() < 1e-30 {
return (0.0, false);
}
let ratio = (j2 / j1).abs();
let is_stable = j2 < 0.0 && ratio > 0.25;
(ratio, is_stable)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hopf_map_north_pole() {
let result = HopfFibration::map(1.0, 0.0, 0.0, 0.0);
assert!(
(result.z - 1.0).abs() < 1e-10,
"Expected north pole, got z = {}",
result.z
);
assert!(result.x.abs() < 1e-10, "Expected x=0, got {}", result.x);
assert!(result.y.abs() < 1e-10, "Expected y=0, got {}", result.y);
}
#[test]
fn test_hopf_map_south_pole() {
let result = HopfFibration::map(0.0, 0.0, 1.0, 0.0);
assert!(
(result.z + 1.0).abs() < 1e-10,
"Expected south pole, got z = {}",
result.z
);
}
#[test]
fn test_hopf_invariant_uniform_state() {
let grid_size = (8, 8, 8);
let cell_size = 1.0e-9;
let q_h = HopfInvariant::calculate_uniform(grid_size, cell_size)
.expect("Failed to calculate Hopf invariant for uniform state");
assert!(
q_h.abs() < 0.1,
"Uniform state should have Q_H ~ 0, got {}",
q_h
);
}
#[test]
fn test_hopfion_magnetization_normalized() {
let hopfion = Hopfion::new((16, 16, 16), 5.0e-9, 1).expect("Failed to create hopfion");
assert!(
hopfion.is_normalized(1e-10),
"All magnetization vectors should be normalized"
);
}
#[test]
fn test_hopfion_energy_finite_and_positive() {
let hopfion = Hopfion::new((12, 12, 12), 3.0e-9, 1).expect("Failed to create hopfion");
let params = HopfionEnergyParams {
exchange_stiffness: 1.0e-11,
dmi_constant: 0.0,
external_field: Vector3::zero(),
saturation_magnetization: 5.8e5,
frustrated_exchange_j2: 0.0,
};
let e_ex = HopfionEnergy::exchange_energy(&hopfion, params.exchange_stiffness)
.expect("Failed to calculate exchange energy");
assert!(e_ex.is_finite(), "Exchange energy must be finite");
assert!(
e_ex >= 0.0,
"Exchange energy must be non-negative, got {}",
e_ex
);
}
#[test]
fn test_profile_function_boundary_conditions() {
let profile = ProfileFunction::Linear;
let radius = 10.0e-9;
let f_at_zero = profile.evaluate(0.0, radius);
assert!(
(f_at_zero - PI).abs() < 1e-10,
"f(0) should be pi, got {}",
f_at_zero
);
let f_at_r = profile.evaluate(radius, radius);
assert!(f_at_r.abs() < 1e-10, "f(R) should be 0, got {}", f_at_r);
let f_beyond = profile.evaluate(2.0 * radius, radius);
assert!(
f_beyond.abs() < 1e-10,
"f(2R) should be 0, got {}",
f_beyond
);
let f_half = profile.evaluate(radius / 2.0, radius);
assert!(
(f_half - PI / 2.0).abs() < 1e-10,
"f(R/2) should be pi/2 for linear profile, got {}",
f_half
);
}
#[test]
fn test_grid_indexing_correctness() {
let hopfion = Hopfion::new((8, 10, 12), 3.0e-9, 1).expect("Failed to create hopfion");
let (nx, ny, _nz) = hopfion.grid_size;
let idx_000 = hopfion
.linear_index(0, 0, 0)
.expect("Index (0,0,0) should be valid");
assert_eq!(idx_000, 0);
let idx_100 = hopfion
.linear_index(1, 0, 0)
.expect("Index (1,0,0) should be valid");
assert_eq!(idx_100, 1);
let idx_010 = hopfion
.linear_index(0, 1, 0)
.expect("Index (0,1,0) should be valid");
assert_eq!(idx_010, nx);
let idx_001 = hopfion
.linear_index(0, 0, 1)
.expect("Index (0,0,1) should be valid");
assert_eq!(idx_001, nx * ny);
assert!(hopfion.linear_index(8, 0, 0).is_none());
assert!(hopfion.linear_index(0, 10, 0).is_none());
assert!(hopfion.linear_index(0, 0, 12).is_none());
}
#[test]
fn test_topological_protection_small_perturbation() {
let mut hopfion = Hopfion::new((10, 10, 10), 3.0e-9, 1).expect("Failed to create hopfion");
let q_h_before =
HopfInvariant::calculate(&hopfion).expect("Failed to compute Q_H before perturbation");
let (nx, ny, nz) = hopfion.grid_size;
let perturbation_magnitude = 0.01;
for iz in 2..nz - 2 {
for iy in 2..ny - 2 {
for ix in 2..nx - 2 {
let m = hopfion.magnetization_at(ix, iy, iz);
let perturbed = Vector3::new(
m.x + perturbation_magnitude * ((ix + iy + iz) as f64 * 0.1).sin(),
m.y + perturbation_magnitude * ((ix + iy + iz) as f64 * 0.2).cos(),
m.z,
);
hopfion
.set_magnetization_at(ix, iy, iz, perturbed)
.expect("Failed to set perturbed magnetization");
}
}
}
let q_h_after =
HopfInvariant::calculate(&hopfion).expect("Failed to compute Q_H after perturbation");
let denom = q_h_before.abs().max(1e-30);
let relative_change = (q_h_after - q_h_before).abs() / denom;
assert!(
relative_change < 0.01,
"Hopf invariant should be robust to small perturbations: \
Q_H_before={:.4}, Q_H_after={:.4}, relative_change={:.6}",
q_h_before,
q_h_after,
relative_change
);
}
#[test]
fn test_toroidal_structure_verification() {
let hopfion = Hopfion::new((20, 20, 20), 5.0e-9, 1).expect("Failed to create hopfion");
let (nx, ny, nz) = hopfion.grid_size;
let cx = nx / 2;
let cy = ny / 2;
let cz = nz / 2;
let m_center = hopfion.magnetization_at(cx, cy, cz);
let m_far = hopfion.magnetization_at(0, 0, 0);
let diff = (m_center - m_far).magnitude();
assert!(
diff > 0.01,
"Hopfion center should differ from corner: diff = {:.6}",
diff
);
if cx + 2 < nx && cy + 2 < ny {
let m_xp = hopfion.magnetization_at(cx + 2, cy, cz);
let m_yp = hopfion.magnetization_at(cx, cy + 2, cz);
let mz_diff = (m_xp.z - m_yp.z).abs();
assert!(
mz_diff < 0.3,
"Toroidal symmetry: mz at (+x) and (+y) should be similar, diff = {:.6}",
mz_diff
);
}
}
#[test]
fn test_hopf_invariant_single_hopfion() {
let hopfion = Hopfion::new((20, 20, 20), 5.0e-9, 1).expect("Failed to create hopfion");
let q_h = HopfInvariant::calculate(&hopfion).expect("Failed to compute Hopf invariant");
assert!(
q_h.is_finite(),
"Hopf invariant must be finite, got {}",
q_h
);
let uniform = Hopfion::uniform(hopfion.grid_size, hopfion.cell_size)
.expect("Failed to create uniform state");
let q_h_uniform =
HopfInvariant::calculate(&uniform).expect("Failed to compute Q_H for uniform");
let hopfion_is_nontrivial = q_h.abs() > q_h_uniform.abs() * 2.0 + 1e-6;
assert!(
hopfion_is_nontrivial || q_h.abs() > 0.01,
"Hopfion Q_H={:.6} should be distinguishable from uniform Q_H={:.6}",
q_h,
q_h_uniform
);
}
#[test]
fn test_toroidal_coord_roundtrip() {
let major_r = 5.0e-9;
let rho = 2.0e-9;
let theta = 0.7;
let phi = 1.3;
let (x, y, z) = ToroidalCoords::to_cartesian(rho, theta, phi, major_r);
let (rho2, theta2, phi2) = ToroidalCoords::from_cartesian(x, y, z, major_r);
assert!(
(rho2 - rho).abs() < 1e-15,
"Rho roundtrip failed: {} vs {}",
rho,
rho2
);
assert!(
(theta2 - theta).abs() < 1e-15,
"Theta roundtrip failed: {} vs {}",
theta,
theta2
);
assert!(
(phi2 - phi).abs() < 1e-15,
"Phi roundtrip failed: {} vs {}",
phi,
phi2
);
}
#[test]
fn test_frustration_assessment() {
let (ratio, is_stable) = HopfionStability::frustration_assessment(1.0e-11, -3.0e-12);
assert!(
(ratio - 0.3).abs() < 1e-10,
"Expected ratio ~0.3, got {}",
ratio
);
assert!(is_stable, "Should be potentially stable at ratio 0.3");
let (ratio2, is_stable2) = HopfionStability::frustration_assessment(1.0e-11, -1.0e-12);
assert!(
(ratio2 - 0.1).abs() < 1e-10,
"Expected ratio ~0.1, got {}",
ratio2
);
assert!(!is_stable2, "Should not be stable at ratio 0.1");
let (_ratio3, is_stable3) = HopfionStability::frustration_assessment(1.0e-11, 5.0e-12);
assert!(
!is_stable3,
"Ferromagnetic NNN should not stabilize hopfions"
);
}
#[test]
fn test_invalid_parameters() {
let result = Hopfion::new((0, 10, 10), 5.0e-9, 1);
assert!(result.is_err(), "Zero grid dimension should fail");
let result = Hopfion::new((10, 10, 10), -1.0e-9, 1);
assert!(result.is_err(), "Negative radius should fail");
let result = Hopfion::new((10, 10, 10), f64::INFINITY, 1);
assert!(result.is_err(), "Infinite radius should fail");
let result = Hopfion::new((10, 10, 10), f64::NAN, 1);
assert!(result.is_err(), "NaN radius should fail");
}
#[test]
fn test_hopf_fibration_inverse_roundtrip() {
let n = Vector3::new(0.5, 0.5, (1.0_f64 - 0.5).sqrt());
let n_norm = n.normalize();
let fiber_angle = 0.42;
let (a, b, c, d) = HopfFibration::inverse_map(&n_norm, fiber_angle);
let n_recovered = HopfFibration::map(a, b, c, d);
assert!(
(n_recovered.x - n_norm.x).abs() < 1e-10,
"x component mismatch: {} vs {}",
n_recovered.x,
n_norm.x
);
assert!(
(n_recovered.y - n_norm.y).abs() < 1e-10,
"y component mismatch: {} vs {}",
n_recovered.y,
n_norm.y
);
assert!(
(n_recovered.z - n_norm.z).abs() < 1e-10,
"z component mismatch: {} vs {}",
n_recovered.z,
n_norm.z
);
}
}