use crate::error::{Error, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LatticeType {
Triangular,
Kagome,
Pyrochlore,
}
#[derive(Debug, Clone)]
pub struct FrustratedLattice {
pub lattice_type: LatticeType,
pub size: (usize, usize),
pub coupling_j: f64,
pub spins: Vec<Vector3<f64>>,
pub neighbors: Vec<Vec<usize>>,
pub positions: Vec<Vector3<f64>>,
pub lattice_constant: f64,
}
#[derive(Debug, Clone)]
pub struct Xorshift64 {
state: u64,
}
impl Xorshift64 {
pub fn new(seed: u64) -> Result<Self> {
if seed == 0 {
return Err(Error::InvalidParameter {
param: "seed".to_string(),
reason: "xorshift64 seed must be nonzero".to_string(),
});
}
Ok(Self { state: seed })
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
}
pub fn random_unit_vector(&mut self) -> Vector3<f64> {
loop {
let u = 2.0 * self.next_f64() - 1.0;
let v = 2.0 * self.next_f64() - 1.0;
let s = u * u + v * v;
if s < 1.0 && s > 1e-30 {
let factor = 2.0 * (1.0 - s).sqrt();
return Vector3::new(u * factor, v * factor, 1.0 - 2.0 * s);
}
}
}
}
impl FrustratedLattice {
pub fn triangular(
nx: usize,
ny: usize,
coupling_j: f64,
lattice_constant: f64,
) -> Result<Self> {
validate_lattice_params(nx, ny, coupling_j, lattice_constant)?;
let n_sites = nx * ny;
let mut positions = Vec::with_capacity(n_sites);
let mut neighbors = Vec::with_capacity(n_sites);
let sqrt3_half = 3.0_f64.sqrt() / 2.0;
for iy in 0..ny {
for ix in 0..nx {
let x = (ix as f64 + 0.5 * iy as f64) * lattice_constant;
let y = iy as f64 * sqrt3_half * lattice_constant;
positions.push(Vector3::new(x, y, 0.0));
}
}
for iy in 0..ny {
for ix in 0..nx {
let idx = iy * nx + ix;
let mut nbrs = Vec::with_capacity(6);
let offsets: [(i64, i64); 6] = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, -1), (-1, 1)];
for (dx, dy) in offsets {
let jx = ((ix as i64 + dx).rem_euclid(nx as i64)) as usize;
let jy = ((iy as i64 + dy).rem_euclid(ny as i64)) as usize;
let j_idx = jy * nx + jx;
if j_idx != idx {
nbrs.push(j_idx);
}
}
nbrs.sort_unstable();
nbrs.dedup();
let _ = idx; neighbors.push(nbrs);
}
}
let spins = vec![Vector3::unit_z(); n_sites];
Ok(Self {
lattice_type: LatticeType::Triangular,
size: (nx, ny),
coupling_j,
spins,
neighbors,
positions,
lattice_constant,
})
}
pub fn kagome(nx: usize, ny: usize, coupling_j: f64, lattice_constant: f64) -> Result<Self> {
validate_lattice_params(nx, ny, coupling_j, lattice_constant)?;
let n_cells = nx * ny;
let n_sites = 3 * n_cells;
let mut positions = Vec::with_capacity(n_sites);
let mut neighbors = Vec::with_capacity(n_sites);
let sqrt3_half = 3.0_f64.sqrt() / 2.0;
let a = lattice_constant;
let sub_offsets = [
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(a / 2.0, 0.0, 0.0),
Vector3::new(a / 4.0, a * sqrt3_half / 2.0, 0.0),
];
for iy in 0..ny {
for ix in 0..nx {
let base_x = (ix as f64 + 0.5 * iy as f64) * a;
let base_y = iy as f64 * sqrt3_half * a;
for sub in &sub_offsets {
positions.push(Vector3::new(base_x + sub.x, base_y + sub.y, 0.0));
}
}
}
let site_idx = |cx: usize, cy: usize, sub: usize| -> usize { 3 * (cy * nx + cx) + sub };
for iy in 0..ny {
for ix in 0..nx {
let ixp = (ix + 1) % nx;
let iyp = (iy + 1) % ny;
let ixm = (ix + nx - 1) % nx;
let iym = (iy + ny - 1) % ny;
let nbrs_0 = vec![
site_idx(ix, iy, 1),
site_idx(ix, iy, 2),
site_idx(ixm, iy, 1),
site_idx(ix, iym, 2),
];
neighbors.push(nbrs_0);
let nbrs_1 = vec![
site_idx(ix, iy, 0),
site_idx(ix, iy, 2),
site_idx(ixp, iy, 0),
site_idx(ixp, iym, 2),
];
neighbors.push(nbrs_1);
let nbrs_2 = vec![
site_idx(ix, iy, 0),
site_idx(ix, iy, 1),
site_idx(ix, iyp, 0),
site_idx(ixm, iyp, 1),
];
neighbors.push(nbrs_2);
}
}
let spins = vec![Vector3::unit_z(); n_sites];
Ok(Self {
lattice_type: LatticeType::Kagome,
size: (nx, ny),
coupling_j,
spins,
neighbors,
positions,
lattice_constant,
})
}
pub fn pyrochlore(
nx: usize,
ny: usize,
coupling_j: f64,
lattice_constant: f64,
) -> Result<Self> {
validate_lattice_params(nx, ny, coupling_j, lattice_constant)?;
let nz = nx.min(ny);
let n_cells = nx * ny * nz;
let n_sites = 4 * n_cells;
let a = lattice_constant;
let sub_offsets = [
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(a / 4.0, a / 4.0, 0.0),
Vector3::new(a / 4.0, 0.0, a / 4.0),
Vector3::new(0.0, a / 4.0, a / 4.0),
];
let mut positions = Vec::with_capacity(n_sites);
let mut neighbors = Vec::with_capacity(n_sites);
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
for sub in &sub_offsets {
positions.push(Vector3::new(
ix as f64 * a + sub.x,
iy as f64 * a + sub.y,
iz as f64 * a + sub.z,
));
}
}
}
}
let site_idx = |cx: usize, cy: usize, cz: usize, sub: usize| -> usize {
4 * ((cz * ny + cy) * nx + cx) + sub
};
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let ixp = (ix + 1) % nx;
let iyp = (iy + 1) % ny;
let izp = (iz + 1) % nz;
let ixm = (ix + nx - 1) % nx;
let iym = (iy + ny - 1) % ny;
let izm = (iz + nz - 1) % nz;
let mut nbrs_0 = vec![
site_idx(ix, iy, iz, 1),
site_idx(ix, iy, iz, 2),
site_idx(ix, iy, iz, 3),
site_idx(ixm, iym, iz, 1),
site_idx(ixm, iy, izm, 2),
site_idx(ix, iym, izm, 3),
];
nbrs_0.sort_unstable();
nbrs_0.dedup();
neighbors.push(nbrs_0);
let mut nbrs_1 = vec![
site_idx(ix, iy, iz, 0),
site_idx(ix, iy, iz, 2),
site_idx(ix, iy, iz, 3),
site_idx(ixp, iyp, iz, 0),
site_idx(ix, iyp, izm, 3),
site_idx(ixp, iy, izm, 2),
];
nbrs_1.sort_unstable();
nbrs_1.dedup();
neighbors.push(nbrs_1);
let mut nbrs_2 = vec![
site_idx(ix, iy, iz, 0),
site_idx(ix, iy, iz, 1),
site_idx(ix, iy, iz, 3),
site_idx(ixp, iy, izp, 0),
site_idx(ixp, iy, izm, 1),
site_idx(ix, iy, izp, 3),
];
nbrs_2.sort_unstable();
nbrs_2.dedup();
neighbors.push(nbrs_2);
let mut nbrs_3 = vec![
site_idx(ix, iy, iz, 0),
site_idx(ix, iy, iz, 1),
site_idx(ix, iy, iz, 2),
site_idx(ix, iyp, izp, 0),
site_idx(ix, iyp, izm, 1),
site_idx(ix, iy, izp, 2),
];
nbrs_3.sort_unstable();
nbrs_3.dedup();
neighbors.push(nbrs_3);
}
}
}
let ising_axes = [
Vector3::new(1.0, 1.0, 1.0).normalize(),
Vector3::new(1.0, -1.0, -1.0).normalize(),
Vector3::new(-1.0, 1.0, -1.0).normalize(),
Vector3::new(-1.0, -1.0, 1.0).normalize(),
];
let mut spins = Vec::with_capacity(n_sites);
for i in 0..n_sites {
spins.push(ising_axes[i % 4]);
}
Ok(Self {
lattice_type: LatticeType::Pyrochlore,
size: (nx, ny),
coupling_j,
spins,
neighbors,
positions,
lattice_constant,
})
}
pub fn num_sites(&self) -> usize {
self.spins.len()
}
pub fn total_energy(&self) -> f64 {
let mut energy = 0.0;
for (i, nbrs) in self.neighbors.iter().enumerate() {
for &j in nbrs {
if j > i {
energy += self.coupling_j * self.spins[i].dot(&self.spins[j]);
}
}
}
energy
}
pub fn site_energy(&self, site: usize) -> f64 {
let mut energy = 0.0;
if site < self.neighbors.len() {
for &j in &self.neighbors[site] {
energy += self.coupling_j * self.spins[site].dot(&self.spins[j]);
}
}
energy
}
pub fn average_magnetization(&self) -> Vector3<f64> {
let n = self.spins.len() as f64;
if n < 1.0 {
return Vector3::zero();
}
let mut sum = Vector3::zero();
for s in &self.spins {
sum = sum + *s;
}
sum * (1.0 / n)
}
pub fn kagome_sublattice_magnetizations(&self) -> Result<[Vector3<f64>; 3]> {
if self.lattice_type != LatticeType::Kagome {
return Err(Error::InvalidParameter {
param: "lattice_type".to_string(),
reason: "sublattice magnetizations only defined for kagome lattice".to_string(),
});
}
let n_cells = self.spins.len() / 3;
let mut mags = [Vector3::zero(); 3];
for (i, spin) in self.spins.iter().enumerate() {
let sub = i % 3;
mags[sub] = mags[sub] + *spin;
}
let inv_n = 1.0 / n_cells as f64;
for m in &mut mags {
*m = *m * inv_n;
}
Ok(mags)
}
pub fn set_120_degree_order(&mut self) {
let sqrt3_half = 3.0_f64.sqrt() / 2.0;
let directions = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(-0.5, sqrt3_half, 0.0),
Vector3::new(-0.5, -sqrt3_half, 0.0),
];
match self.lattice_type {
LatticeType::Triangular => {
let nx = self.size.0;
for (i, spin) in self.spins.iter_mut().enumerate() {
let ix = i % nx;
let iy = i / nx;
let sub = (ix + 2 * iy) % 3;
*spin = directions[sub];
}
},
LatticeType::Kagome => {
for (i, spin) in self.spins.iter_mut().enumerate() {
*spin = directions[i % 3];
}
},
LatticeType::Pyrochlore => {
for (i, spin) in self.spins.iter_mut().enumerate() {
*spin = directions[i % 3];
}
},
}
}
pub fn metropolis_mc(&mut self, temperature: f64, n_sweeps: usize, seed: u64) -> Result<f64> {
if temperature < 0.0 {
return Err(Error::InvalidParameter {
param: "temperature".to_string(),
reason: "temperature must be non-negative".to_string(),
});
}
let mut rng = Xorshift64::new(seed)?;
let n_sites = self.num_sites();
let beta = if temperature > 1e-30 {
1.0 / (crate::constants::KB * temperature)
} else {
f64::INFINITY
};
for _sweep in 0..n_sweeps {
for _step in 0..n_sites {
let site = (rng.next_u64() as usize) % n_sites;
let old_energy = self.site_energy(site);
let old_spin = self.spins[site];
let new_spin = rng.random_unit_vector();
self.spins[site] = new_spin;
let new_energy = self.site_energy(site);
let delta_e = new_energy - old_energy;
if delta_e > 0.0 && !beta.is_infinite() {
let acceptance = (-beta * delta_e).exp();
if rng.next_f64() >= acceptance {
self.spins[site] = old_spin;
}
}
if delta_e > 0.0 && beta.is_infinite() {
self.spins[site] = old_spin;
}
}
}
Ok(self.total_energy() / n_sites as f64)
}
}
pub fn frustration_parameter(curie_weiss_temp: f64, neel_temp: f64) -> Result<f64> {
if neel_temp <= 0.0 {
return Err(Error::InvalidParameter {
param: "neel_temp".to_string(),
reason: "Néel temperature must be positive".to_string(),
});
}
Ok(curie_weiss_temp.abs() / neel_temp)
}
pub fn curie_weiss_temperature(coordination_number: usize, coupling_j: f64, spin_s: f64) -> f64 {
let z = coordination_number as f64;
-z * coupling_j * spin_s * (spin_s + 1.0) / (3.0 * crate::constants::KB)
}
fn validate_lattice_params(
nx: usize,
ny: usize,
coupling_j: f64,
lattice_constant: f64,
) -> Result<()> {
if nx == 0 || ny == 0 {
return Err(Error::InvalidParameter {
param: "size".to_string(),
reason: "lattice dimensions must be nonzero".to_string(),
});
}
if coupling_j <= 0.0 {
return Err(Error::InvalidParameter {
param: "coupling_j".to_string(),
reason: "antiferromagnetic coupling must be positive (J > 0)".to_string(),
});
}
if lattice_constant <= 0.0 {
return Err(Error::InvalidParameter {
param: "lattice_constant".to_string(),
reason: "lattice constant must be positive".to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_triangular_lattice_creation() {
let lat = FrustratedLattice::triangular(6, 6, 1e-21, 3e-10)
.expect("failed to create triangular lattice");
assert_eq!(lat.num_sites(), 36);
assert_eq!(lat.lattice_type, LatticeType::Triangular);
}
#[test]
fn test_triangular_neighbor_count() {
let lat = FrustratedLattice::triangular(8, 8, 1e-21, 3e-10)
.expect("failed to create triangular lattice");
for (i, nbrs) in lat.neighbors.iter().enumerate() {
assert_eq!(
nbrs.len(),
6,
"site {} has {} neighbors, expected 6",
i,
nbrs.len()
);
}
}
#[test]
fn test_kagome_lattice_creation() {
let lat =
FrustratedLattice::kagome(4, 4, 1e-21, 5e-10).expect("failed to create kagome lattice");
assert_eq!(lat.num_sites(), 48); assert_eq!(lat.lattice_type, LatticeType::Kagome);
}
#[test]
fn test_kagome_neighbor_count() {
let lat =
FrustratedLattice::kagome(6, 6, 1e-21, 5e-10).expect("failed to create kagome lattice");
for (i, nbrs) in lat.neighbors.iter().enumerate() {
assert_eq!(
nbrs.len(),
4,
"site {} has {} neighbors, expected 4",
i,
nbrs.len()
);
}
}
#[test]
fn test_pyrochlore_lattice_creation() {
let lat = FrustratedLattice::pyrochlore(3, 3, 1e-21, 1e-9)
.expect("failed to create pyrochlore lattice");
assert_eq!(lat.num_sites(), 108);
assert_eq!(lat.lattice_type, LatticeType::Pyrochlore);
}
#[test]
fn test_frustration_parameter_known_materials() {
let f =
frustration_parameter(-500.0, 3.5).expect("failed to compute frustration parameter");
assert!((f - 142.857).abs() < 0.1, "f = {}, expected ~143", f);
let f_conv =
frustration_parameter(-100.0, 90.0).expect("failed to compute frustration parameter");
assert!(
f_conv > 1.0 && f_conv < 2.0,
"f = {}, expected ~1.1",
f_conv
);
}
#[test]
fn test_frustration_parameter_invalid() {
let result = frustration_parameter(-100.0, 0.0);
assert!(result.is_err());
let result = frustration_parameter(-100.0, -10.0);
assert!(result.is_err());
}
#[test]
fn test_120_degree_order_energy() {
let mut lat =
FrustratedLattice::triangular(6, 6, 1.0, 1e-10).expect("failed to create lattice");
lat.set_120_degree_order();
let energy_per_site = lat.total_energy() / lat.num_sites() as f64;
assert!(
(energy_per_site - (-1.5)).abs() < 0.01,
"energy per site = {}, expected -1.5",
energy_per_site
);
}
#[test]
fn test_mc_energy_decreases() {
let mut lat =
FrustratedLattice::triangular(6, 6, 1e-21, 3e-10).expect("failed to create lattice");
let mut rng = Xorshift64::new(42).expect("failed to create rng");
for spin in lat.spins.iter_mut() {
*spin = rng.random_unit_vector();
}
let initial_energy = lat.total_energy();
let _final_e_per_site = lat
.metropolis_mc(1.0, 100, 12345)
.expect("MC simulation failed");
let final_energy = lat.total_energy();
assert!(
final_energy <= initial_energy + 1e-30,
"energy increased: {} -> {}",
initial_energy,
final_energy
);
}
#[test]
fn test_lattice_size_correctness() {
let lat =
FrustratedLattice::triangular(5, 7, 1e-21, 1e-10).expect("failed to create lattice");
assert_eq!(lat.num_sites(), 35);
let lat = FrustratedLattice::kagome(4, 5, 1e-21, 1e-10).expect("failed to create lattice");
assert_eq!(lat.num_sites(), 60);
let lat =
FrustratedLattice::pyrochlore(3, 4, 1e-21, 1e-10).expect("failed to create lattice");
assert_eq!(lat.num_sites(), 144); }
#[test]
fn test_xorshift64_deterministic() {
let mut rng1 = Xorshift64::new(42).expect("failed to create rng");
let mut rng2 = Xorshift64::new(42).expect("failed to create rng");
for _ in 0..100 {
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
}
#[test]
fn test_xorshift64_zero_seed_error() {
let result = Xorshift64::new(0);
assert!(result.is_err());
}
#[test]
fn test_invalid_lattice_params() {
assert!(FrustratedLattice::triangular(0, 5, 1e-21, 1e-10).is_err());
assert!(FrustratedLattice::triangular(5, 5, -1e-21, 1e-10).is_err());
assert!(FrustratedLattice::triangular(5, 5, 1e-21, 0.0).is_err());
}
#[test]
fn test_curie_weiss_temperature() {
let theta = curie_weiss_temperature(6, 1e-21, 0.5);
assert!(theta < 0.0, "CW temperature should be negative for AFM");
let expected = -6.0 * 1e-21 * 0.5 * 1.5 / (3.0 * crate::constants::KB);
assert!(
(theta - expected).abs() / expected.abs() < 1e-10,
"theta = {}, expected {}",
theta,
expected
);
}
}