use std::f64::consts::PI;
use crate::effect::topological_hall::{TopologicalHall, PHI_0};
use crate::error::{Error, Result};
use crate::frustrated::lattice::{FrustratedLattice, LatticeType};
use crate::vector3::Vector3;
const SOLID_ANGLE_CHIRALITY_EPSILON: f64 = 1e-12;
const MIN_PLAQUETTE_AREA: f64 = 1e-30;
pub fn scalar_spin_chirality(s_i: Vector3<f64>, s_j: Vector3<f64>, s_k: Vector3<f64>) -> f64 {
s_i.dot(&s_j.cross(&s_k))
}
pub fn berg_luscher_solid_angle(chi: f64, dot_ij: f64, dot_jk: f64, dot_ki: f64) -> f64 {
if chi.abs() < SOLID_ANGLE_CHIRALITY_EPSILON {
return 0.0;
}
2.0 * chi.atan2(1.0 + dot_ij + dot_jk + dot_ki)
}
#[derive(Debug, Clone, Copy)]
pub struct Plaquette {
pub sites: (usize, usize, usize),
pub area: f64,
pub chirality: f64,
pub solid_angle: f64,
pub emergent_field: f64,
}
#[derive(Debug, Clone)]
pub struct FrustratedTransport {
pub plaquettes: Vec<Plaquette>,
}
impl FrustratedTransport {
pub fn from_lattice(lattice: &FrustratedLattice) -> Result<Self> {
validate_lattice_arrays(lattice)?;
let raw_triangles = enumerate_plaquettes(&lattice.neighbors);
let lattice_vectors = periodicity_vectors(lattice);
let mut plaquettes = Vec::with_capacity(raw_triangles.len());
for (i0, j0, k0) in raw_triangles {
let e_i_j = minimum_image_displacement(&lattice.positions, &lattice_vectors, i0, j0);
let e_i_k = minimum_image_displacement(&lattice.positions, &lattice_vectors, i0, k0);
let (i, j, k, e1, e2) = orient_ccw(i0, j0, k0, e_i_j, e_i_k);
let area = 0.5 * e1.cross(&e2).magnitude();
if !area.is_finite() || area <= MIN_PLAQUETTE_AREA {
return Err(Error::NumericalError {
description: format!(
"degenerate plaquette ({}, {}, {}) has near-zero area {:.3e} m^2; \
positions may be colinear",
i, j, k, area
),
});
}
let s_i = lattice.spins[i];
let s_j = lattice.spins[j];
let s_k = lattice.spins[k];
let chirality = scalar_spin_chirality(s_i, s_j, s_k);
let dot_ij = s_i.dot(&s_j);
let dot_jk = s_j.dot(&s_k);
let dot_ki = s_k.dot(&s_i);
let solid_angle = berg_luscher_solid_angle(chirality, dot_ij, dot_jk, dot_ki);
let emergent_field =
TopologicalHall::emergent_field_from_solid_angle(solid_angle, area)?;
plaquettes.push(Plaquette {
sites: (i, j, k),
area,
chirality,
solid_angle,
emergent_field,
});
}
Ok(Self { plaquettes })
}
pub fn num_plaquettes(&self) -> usize {
self.plaquettes.len()
}
pub fn total_chirality(&self) -> f64 {
self.plaquettes.iter().map(|p| p.chirality).sum()
}
pub fn mean_chirality(&self) -> f64 {
let n = self.plaquettes.len();
if n == 0 {
0.0
} else {
self.total_chirality() / n as f64
}
}
pub fn total_solid_angle(&self) -> f64 {
self.plaquettes.iter().map(|p| p.solid_angle).sum()
}
pub fn total_area(&self) -> f64 {
self.plaquettes.iter().map(|p| p.area).sum()
}
pub fn effective_topological_charge_density(&self) -> f64 {
let area = self.total_area();
if area > 0.0 {
self.total_solid_angle() / (4.0 * PI * area)
} else {
0.0
}
}
pub fn mean_emergent_field(&self) -> f64 {
PHI_0 * self.effective_topological_charge_density()
}
pub fn hall_resistivity(&self, effect: &TopologicalHall) -> f64 {
effect.hall_resistivity_from_charge_density(self.effective_topological_charge_density())
}
}
pub fn frustration_hall_response(
lattice: &FrustratedLattice,
effect: &TopologicalHall,
) -> Result<f64> {
Ok(FrustratedTransport::from_lattice(lattice)?.hall_resistivity(effect))
}
fn enumerate_plaquettes(neighbors: &[Vec<usize>]) -> Vec<(usize, usize, usize)> {
let mut triangles = Vec::new();
for (i, nbrs_i) in neighbors.iter().enumerate() {
let mut higher: Vec<usize> = nbrs_i.iter().copied().filter(|&x| x > i).collect();
higher.sort_unstable();
higher.dedup();
for (a, &j) in higher.iter().enumerate() {
for &k in &higher[(a + 1)..] {
if neighbors[j].contains(&k) {
triangles.push((i, j, k));
}
}
}
}
triangles
}
fn orient_ccw(
i: usize,
j: usize,
k: usize,
e_i_j: Vector3<f64>,
e_i_k: Vector3<f64>,
) -> (usize, usize, usize, Vector3<f64>, Vector3<f64>) {
let normal_z = e_i_j.x * e_i_k.y - e_i_j.y * e_i_k.x;
if normal_z >= 0.0 {
(i, j, k, e_i_j, e_i_k)
} else {
(i, k, j, e_i_k, e_i_j)
}
}
fn periodicity_vectors(lattice: &FrustratedLattice) -> Vec<Vector3<f64>> {
let a = lattice.lattice_constant;
let (nx, ny) = lattice.size;
let sqrt3_half = 3.0_f64.sqrt() / 2.0;
match lattice.lattice_type {
LatticeType::Triangular | LatticeType::Kagome => {
vec![
Vector3::new(nx as f64 * a, 0.0, 0.0),
Vector3::new(ny as f64 * 0.5 * a, ny as f64 * sqrt3_half * a, 0.0),
]
},
LatticeType::Pyrochlore => {
let nz = nx.min(ny);
vec![
Vector3::new(nx as f64 * a, 0.0, 0.0),
Vector3::new(0.0, ny as f64 * a, 0.0),
Vector3::new(0.0, 0.0, nz as f64 * a),
]
},
}
}
fn minimum_image_displacement(
positions: &[Vector3<f64>],
lattice_vectors: &[Vector3<f64>],
i: usize,
j: usize,
) -> Vector3<f64> {
let naive = positions[j] - positions[i];
let coeffs: [f64; 3] = [-1.0, 0.0, 1.0];
let mut best = naive;
let mut best_mag_sq = naive.magnitude_squared();
let mut consider = |candidate: Vector3<f64>| {
let mag_sq = candidate.magnitude_squared();
if mag_sq < best_mag_sq {
best = candidate;
best_mag_sq = mag_sq;
}
};
match lattice_vectors {
[l1, l2] => {
for &n1 in &coeffs {
for &n2 in &coeffs {
consider(naive + *l1 * n1 + *l2 * n2);
}
}
},
[l1, l2, l3] => {
for &n1 in &coeffs {
for &n2 in &coeffs {
for &n3 in &coeffs {
consider(naive + *l1 * n1 + *l2 * n2 + *l3 * n3);
}
}
}
},
_ => {},
}
best
}
fn validate_lattice_arrays(lattice: &FrustratedLattice) -> Result<()> {
let n = lattice.spins.len();
if lattice.positions.len() != n || lattice.neighbors.len() != n {
return Err(Error::DimensionMismatch {
expected: format!("spins/positions/neighbors all of length {}", n),
actual: format!(
"spins={}, positions={}, neighbors={}",
n,
lattice.positions.len(),
lattice.neighbors.len()
),
});
}
for (site, nbrs) in lattice.neighbors.iter().enumerate() {
for &j in nbrs {
if j >= n {
return Err(Error::DimensionMismatch {
expected: format!("neighbor index < {}", n),
actual: format!("site {} lists neighbor index {}", site, j),
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frustrated::lattice::Xorshift64;
use std::f64::consts::{FRAC_PI_2, FRAC_PI_3};
#[test]
fn test_plaquette_count_triangular() {
let lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert_eq!(transport.num_plaquettes(), 2 * lat.num_sites());
}
#[test]
fn test_plaquette_count_kagome() {
let lat =
FrustratedLattice::kagome(4, 4, 1.0, 1e-10).expect("failed to create kagome lattice");
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
let n_cells = 4 * 4;
assert_eq!(transport.num_plaquettes(), 2 * n_cells);
}
#[test]
fn test_coplanar_120_degree_gives_zero_chirality_and_hall_response() {
let mut lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
lat.set_120_degree_order();
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert!(transport.num_plaquettes() > 0);
for p in &transport.plaquettes {
assert!(
p.chirality.abs() < 1e-9,
"coplanar plaquette {:?} has nonzero chirality {}",
p.sites,
p.chirality
);
assert!(
p.solid_angle.abs() < 1e-9,
"coplanar plaquette {:?} has nonzero solid angle {}",
p.sites,
p.solid_angle
);
}
assert!(transport.total_chirality().abs() < 1e-9);
assert!(transport.total_solid_angle().abs() < 1e-9);
let mnsi = TopologicalHall::mnsi();
let rho = transport.hall_resistivity(&mnsi);
assert!(
rho.abs() < 1e-9,
"expected ~zero Hall response, got {}",
rho
);
}
#[test]
fn test_triangular_umbrella_per_plaquette_nonzero_but_net_cancels() {
let mut lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
lat.set_umbrella_order(FRAC_PI_3);
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert!(transport.num_plaquettes() > 0);
let theta = FRAC_PI_3;
let expected_magnitude = 1.5 * 3.0_f64.sqrt() * theta.sin().powi(2) * theta.cos();
assert!(expected_magnitude > 0.5, "sanity check on analytic value");
let mut n_positive = 0usize;
let mut n_negative = 0usize;
for p in &transport.plaquettes {
assert!(
(p.chirality.abs() - expected_magnitude).abs() < 1e-6,
"plaquette {:?} chirality {} does not match analytic magnitude {}",
p.sites,
p.chirality,
expected_magnitude
);
if p.chirality > 0.0 {
n_positive += 1;
} else {
n_negative += 1;
}
}
assert_eq!(n_positive + n_negative, transport.num_plaquettes());
assert!(
n_positive > 0 && n_negative > 0,
"expected both triangle orientations to be present"
);
assert_eq!(
n_positive, n_negative,
"up/down triangles should be equinumerous"
);
assert!(transport.total_chirality().abs() < 1e-9);
let mnsi = TopologicalHall::mnsi();
assert!(transport.hall_resistivity(&mnsi).abs() < 1e-9);
}
#[test]
fn test_triangular_random_noncoplanar_gives_nonzero_net_response() {
let mut lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
let mut rng = Xorshift64::new(2024).expect("failed to create rng");
for spin in lat.spins.iter_mut() {
*spin = rng.random_unit_vector();
}
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert!(transport.total_chirality().abs() > 0.5);
let mnsi = TopologicalHall::mnsi();
assert!(transport.hall_resistivity(&mnsi).abs() > 0.0);
}
#[test]
fn test_kagome_umbrella_gives_nonzero_uniform_sign_chirality_and_hall_response() {
let mut lat =
FrustratedLattice::kagome(4, 4, 1.0, 1e-10).expect("failed to create kagome lattice");
let theta = FRAC_PI_3;
lat.set_umbrella_order(theta);
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert!(transport.num_plaquettes() > 0);
let expected_magnitude = 1.5 * 3.0_f64.sqrt() * theta.sin().powi(2) * theta.cos();
let first_sign = transport.plaquettes[0].chirality.signum();
for p in &transport.plaquettes {
assert!(
(p.chirality.abs() - expected_magnitude).abs() < 1e-6,
"plaquette {:?} chirality {} does not match analytic magnitude {}",
p.sites,
p.chirality,
expected_magnitude
);
assert_eq!(
p.chirality.signum(),
first_sign,
"plaquette {:?} chirality sign is not coherent with the rest of the lattice",
p.sites
);
}
assert!(transport.total_chirality().abs() > 1.0);
assert!(transport.total_solid_angle().abs() > 1.0);
assert!(transport.mean_emergent_field().is_finite());
let mnsi = TopologicalHall::mnsi();
let rho = transport.hall_resistivity(&mnsi);
assert!(rho.is_finite());
assert!(
rho.abs() > 0.0,
"expected nonzero Hall response for kagome umbrella order"
);
}
#[test]
fn test_sign_reversal_under_global_mirror() {
let mut lat =
FrustratedLattice::kagome(4, 4, 1.0, 1e-10).expect("failed to create kagome lattice");
lat.set_umbrella_order(FRAC_PI_3);
let mnsi = TopologicalHall::mnsi();
let transport_before = FrustratedTransport::from_lattice(&lat).expect("transport failed");
let rho_before = transport_before.hall_resistivity(&mnsi);
assert!(rho_before.abs() > 0.0);
for spin in lat.spins.iter_mut() {
spin.z = -spin.z;
}
let transport_after = FrustratedTransport::from_lattice(&lat).expect("transport failed");
let rho_after = transport_after.hall_resistivity(&mnsi);
assert!(rho_after.abs() > 0.0);
assert!(
(rho_before + rho_after).abs() < 1e-9 * rho_before.abs().max(1e-9),
"mirroring should reverse the sign of the Hall response: before={:.6e}, after={:.6e}",
rho_before,
rho_after
);
}
#[test]
fn test_frustration_hall_response_matches_struct_method() {
let mut lat = FrustratedLattice::triangular(5, 5, 1.0, 1e-10)
.expect("failed to create triangular lattice");
lat.set_umbrella_order(FRAC_PI_3);
let mnsi = TopologicalHall::mnsi();
let via_free_fn = frustration_hall_response(&lat, &mnsi).expect("free fn failed");
let via_struct = FrustratedTransport::from_lattice(&lat)
.expect("transport failed")
.hall_resistivity(&mnsi);
assert!((via_free_fn - via_struct).abs() < 1e-30);
}
#[test]
fn test_umbrella_at_right_angle_matches_coplanar_zero() {
let mut lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
lat.set_umbrella_order(FRAC_PI_2);
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
assert!(transport.total_chirality().abs() < 1e-9);
let mnsi = TopologicalHall::mnsi();
assert!(transport.hall_resistivity(&mnsi).abs() < 1e-9);
}
#[test]
fn test_chirality_and_solid_angle_are_bounded_for_relaxed_configuration() {
let mut lat = FrustratedLattice::triangular(6, 6, 1.0, 1e-10)
.expect("failed to create triangular lattice");
let mut rng = Xorshift64::new(7).expect("failed to create rng");
for spin in lat.spins.iter_mut() {
*spin = rng.random_unit_vector();
}
let _ = lat
.metropolis_mc(0.5, 20, 99)
.expect("MC simulation failed");
let transport = FrustratedTransport::from_lattice(&lat).expect("transport failed");
for p in &transport.plaquettes {
assert!(
p.chirality.abs() <= 1.0 + 1e-9,
"chirality {} out of the physical [-1,1] bound",
p.chirality
);
assert!(
p.solid_angle.abs() <= 2.0 * PI + 1e-6,
"solid angle {} out of the physical [-2*pi,2*pi] bound",
p.solid_angle
);
assert!(p.emergent_field.is_finite());
}
}
#[test]
fn test_from_lattice_rejects_mismatched_arrays() {
let mut lat = FrustratedLattice::triangular(3, 3, 1.0, 1e-10)
.expect("failed to create triangular lattice");
lat.positions.pop();
assert!(FrustratedTransport::from_lattice(&lat).is_err());
}
#[test]
fn test_from_lattice_rejects_out_of_bounds_neighbor_index() {
let mut lat = FrustratedLattice::triangular(3, 3, 1.0, 1e-10)
.expect("failed to create triangular lattice");
let n = lat.num_sites();
lat.neighbors[0].push(n + 100);
assert!(FrustratedTransport::from_lattice(&lat).is_err());
}
}