use super::lattice::{FrustratedLattice, Xorshift64};
use crate::constants::{KB, MU_0, NA};
use crate::error::{Error, Result};
use crate::vector3::Vector3;
const R_GAS: f64 = NA * KB;
#[derive(Debug, Clone)]
pub struct SpinIceParams {
pub j_nn: f64,
pub d_nn: f64,
pub j_eff: f64,
pub moment: f64,
pub nn_distance: f64,
pub name: String,
}
impl SpinIceParams {
pub fn dy2ti2o7() -> Self {
Self {
j_nn: -3.72,
d_nn: 2.35,
j_eff: -3.72 + 2.35,
moment: 10.0,
nn_distance: 3.54e-10, name: "Dy2Ti2O7".to_string(),
}
}
pub fn ho2ti2o7() -> Self {
Self {
j_nn: -1.56,
d_nn: 2.35,
j_eff: -1.56 + 2.35,
moment: 10.0,
nn_distance: 3.54e-10,
name: "Ho2Ti2O7".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Tetrahedron {
pub site_indices: [usize; 4],
pub ising_axes: [Vector3<f64>; 4],
}
#[derive(Debug, Clone)]
pub struct SpinIce {
pub lattice: FrustratedLattice,
pub params: SpinIceParams,
pub tetrahedra: Vec<Tetrahedron>,
pub ising_spins: Vec<f64>,
}
impl SpinIce {
pub fn new(nx: usize, ny: usize, params: SpinIceParams) -> Result<Self> {
let coupling_j = params.j_eff.abs() * KB; let lattice =
FrustratedLattice::pyrochlore(nx, ny, coupling_j, params.nn_distance * 8.0_f64.sqrt())?;
let n_sites = lattice.num_sites();
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 nz = nx.min(ny);
let n_cells = nx * ny * nz;
let mut tetrahedra = Vec::with_capacity(n_cells);
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let base = 4 * ((iz * ny + iy) * nx + ix);
let tet = Tetrahedron {
site_indices: [base, base + 1, base + 2, base + 3],
ising_axes,
};
tetrahedra.push(tet);
}
}
}
let ising_spins = vec![1.0; n_sites];
let mut ice = Self {
lattice,
params,
tetrahedra,
ising_spins,
};
ice.update_spin_vectors();
Ok(ice)
}
fn update_spin_vectors(&mut self) {
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(),
];
for (i, spin) in self.lattice.spins.iter_mut().enumerate() {
let sub = i % 4;
*spin = ising_axes[sub] * self.ising_spins[i];
}
}
pub fn check_ice_rule(&self, tet_index: usize) -> bool {
if tet_index >= self.tetrahedra.len() {
return false;
}
let tet = &self.tetrahedra[tet_index];
let mut sum = 0.0;
for &idx in &tet.site_indices {
if idx < self.ising_spins.len() {
sum += self.ising_spins[idx];
}
}
sum.abs() < 0.5
}
pub fn count_ice_rule_violations(&self) -> usize {
(0..self.tetrahedra.len())
.filter(|&i| !self.check_ice_rule(i))
.count()
}
pub fn ice_rule_fraction(&self) -> f64 {
if self.tetrahedra.is_empty() {
return 0.0;
}
let satisfied = self.tetrahedra.len() - self.count_ice_rule_violations();
satisfied as f64 / self.tetrahedra.len() as f64
}
pub fn initialize_ice_state(&mut self, seed: u64) -> Result<()> {
let mut rng = Xorshift64::new(seed)?;
for spin in self.ising_spins.iter_mut() {
*spin = if rng.next_f64() < 0.5 { 1.0 } else { -1.0 };
}
for tet_idx in 0..self.tetrahedra.len() {
let tet = &self.tetrahedra[tet_idx];
let indices = tet.site_indices;
let sum: f64 = indices.iter().map(|&i| self.ising_spins[i]).sum();
if (sum - 0.0).abs() > 0.5 {
let n_to_flip = (sum.abs() / 2.0).round() as usize;
let mut flipped = 0;
for &idx in &indices {
if flipped >= n_to_flip {
break;
}
if (sum > 0.0 && self.ising_spins[idx] > 0.0)
|| (sum < 0.0 && self.ising_spins[idx] < 0.0)
{
self.ising_spins[idx] = -self.ising_spins[idx];
flipped += 1;
}
}
}
}
self.update_spin_vectors();
Ok(())
}
pub fn constrained_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.lattice.num_sites();
let beta = if temperature > 1e-30 {
1.0 / (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_ising = self.ising_spins[site];
self.ising_spins[site] = -old_ising;
let mut violates = false;
for (tet_idx, tet) in self.tetrahedra.iter().enumerate() {
if tet.site_indices.contains(&site) && !self.check_ice_rule(tet_idx) {
violates = true;
break;
}
}
if violates {
self.ising_spins[site] = old_ising;
continue;
}
let mut neighbor_sum = 0.0;
if site < self.lattice.neighbors.len() {
for &j in &self.lattice.neighbors[site] {
if j < self.ising_spins.len() {
neighbor_sum += self.ising_spins[j];
}
}
}
let delta_e = -2.0 * self.params.j_eff * KB * (-old_ising) * neighbor_sum;
if delta_e > 0.0 {
if beta.is_infinite() {
self.ising_spins[site] = old_ising;
} else {
let acceptance = (-beta * delta_e).exp();
if rng.next_f64() >= acceptance {
self.ising_spins[site] = old_ising;
}
}
}
}
}
self.update_spin_vectors();
Ok(self.lattice.total_energy() / n_sites as f64)
}
pub fn total_magnetization(&self) -> Vector3<f64> {
self.lattice.average_magnetization()
}
}
pub fn pauling_entropy() -> f64 {
0.5 * R_GAS * (1.5_f64).ln()
}
pub fn pauling_entropy_per_spin_kb() -> f64 {
0.5 * (1.5_f64).ln()
}
pub fn monopole_creation_energy(j_eff: f64, moment_mu_b: f64, nn_distance: f64) -> Result<f64> {
if nn_distance <= 0.0 {
return Err(Error::InvalidParameter {
param: "nn_distance".to_string(),
reason: "nearest-neighbor distance must be positive".to_string(),
});
}
let mu = moment_mu_b * crate::constants::MU_B;
let energy = 2.0 * j_eff.abs() * KB
+ MU_0 * mu * mu / (4.0 * std::f64::consts::PI * nn_distance.powi(3));
Ok(energy)
}
pub fn monopole_coulomb_interaction(
separation: f64,
moment_mu_b: f64,
diamond_lattice_const: f64,
) -> Result<f64> {
if separation <= 0.0 {
return Err(Error::InvalidParameter {
param: "separation".to_string(),
reason: "monopole separation must be positive".to_string(),
});
}
if diamond_lattice_const <= 0.0 {
return Err(Error::InvalidParameter {
param: "diamond_lattice_const".to_string(),
reason: "lattice constant must be positive".to_string(),
});
}
let mu = moment_mu_b * crate::constants::MU_B;
let q_m = 2.0 * mu / diamond_lattice_const;
let energy = MU_0 * q_m * q_m / (4.0 * std::f64::consts::PI * separation);
Ok(energy)
}
pub fn monopole_density_factor(temperature: f64, creation_energy: f64) -> Result<f64> {
if temperature <= 0.0 {
return Err(Error::InvalidParameter {
param: "temperature".to_string(),
reason: "temperature must be positive for thermal activation".to_string(),
});
}
let exponent = -creation_energy / (2.0 * KB * temperature);
Ok(exponent.exp())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frustrated::lattice::LatticeType;
#[test]
fn test_pauling_entropy() {
let s_p = pauling_entropy();
let expected = 0.5 * R_GAS * (1.5_f64).ln();
assert!(
(s_p - expected).abs() < 1e-10,
"Pauling entropy = {}, expected {}",
s_p,
expected
);
assert!(
(s_p - 1.6854).abs() < 0.01,
"Pauling entropy = {} J/(mol·K), expected ~1.685",
s_p
);
}
#[test]
fn test_pauling_entropy_per_spin() {
let s_per_spin = pauling_entropy_per_spin_kb();
let expected = 0.5 * (1.5_f64).ln();
assert!(
(s_per_spin - expected).abs() < 1e-15,
"Pauling entropy per spin = {}, expected {}",
s_per_spin,
expected
);
}
#[test]
fn test_spin_ice_creation() {
let params = SpinIceParams::dy2ti2o7();
let ice = SpinIce::new(3, 3, params).expect("failed to create spin ice");
assert_eq!(ice.lattice.lattice_type, LatticeType::Pyrochlore);
assert!(ice.lattice.num_sites() > 0);
}
#[test]
fn test_ice_rules_after_initialization() {
let params = SpinIceParams::dy2ti2o7();
let mut ice = SpinIce::new(3, 3, params).expect("failed to create spin ice");
ice.initialize_ice_state(42)
.expect("failed to init ice state");
let fraction = ice.ice_rule_fraction();
assert!(
fraction > 0.5,
"ice rule fraction = {}, expected > 0.5 after initialization",
fraction
);
}
#[test]
fn test_monopole_energy_positive() {
let params = SpinIceParams::dy2ti2o7();
let energy = monopole_creation_energy(params.j_eff, params.moment, params.nn_distance)
.expect("failed to compute monopole energy");
assert!(
energy > 0.0,
"monopole creation energy = {}, must be positive",
energy
);
}
#[test]
fn test_monopole_coulomb_positive() {
let separation = 5e-10; let a_d = 4.3e-10; let energy = monopole_coulomb_interaction(separation, 10.0, a_d)
.expect("failed to compute Coulomb energy");
assert!(
energy > 0.0,
"Coulomb energy = {}, must be positive (repulsive for like charges)",
energy
);
}
#[test]
fn test_monopole_coulomb_decreases_with_distance() {
let a_d = 4.3e-10;
let e1 = monopole_coulomb_interaction(5e-10, 10.0, a_d).expect("close distance");
let e2 = monopole_coulomb_interaction(10e-10, 10.0, a_d).expect("far distance");
assert!(
e1 > e2,
"Coulomb energy should decrease with distance: {} > {}",
e1,
e2
);
}
#[test]
fn test_monopole_density_decreases_with_lower_temp() {
let e_create = 6e-23; let n_high = monopole_density_factor(10.0, e_create).expect("high T");
let n_low = monopole_density_factor(1.0, e_create).expect("low T");
assert!(
n_high > n_low,
"monopole density should increase with temperature"
);
}
#[test]
fn test_spin_ice_params_dy2ti2o7() {
let p = SpinIceParams::dy2ti2o7();
assert!((p.moment - 10.0).abs() < 0.1);
assert!(p.j_nn < 0.0); assert!(p.d_nn > 0.0); assert!((p.j_eff - (p.j_nn + p.d_nn)).abs() < 1e-10);
}
#[test]
fn test_spin_ice_params_ho2ti2o7() {
let p = SpinIceParams::ho2ti2o7();
assert!((p.moment - 10.0).abs() < 0.1);
assert!(p.j_eff > 0.0); }
#[test]
fn test_monopole_energy_invalid_distance() {
let result = monopole_creation_energy(1.0, 10.0, 0.0);
assert!(result.is_err());
let result = monopole_creation_energy(1.0, 10.0, -1.0);
assert!(result.is_err());
}
}