use crate::geometry::Lattice;
use crate::mcmc::{threshold_f64, uniform_draw};
use rand::Rng;
use rand_xoshiro::Xoshiro256StarStar;
use std::cell::RefCell;
use std::ops::{Deref, DerefMut};
#[derive(Clone, Copy)]
pub(crate) struct BondSampler {
neg_rate: f64,
unit: i64,
}
impl BondSampler {
#[inline]
pub(crate) fn new(rate: f32) -> Self {
let neg_rate = -f64::from(rate);
Self {
neg_rate,
unit: Self::general_threshold(neg_rate, 1.0),
}
}
#[inline]
fn general_threshold(neg_rate: f64, x: f32) -> i64 {
threshold_f64(1.0 - (f64::from(x) * neg_rate).exp())
}
#[inline]
pub(crate) fn sample(self, x: f32, rng: &mut Xoshiro256StarStar) -> bool {
let threshold = if x == 1.0 {
self.unit
} else {
Self::general_threshold(self.neg_rate, x)
};
uniform_draw(rng) < threshold
}
}
#[derive(Default)]
pub(super) struct UfStorage {
pub(super) parent: Vec<u32>,
pub(super) rank: Vec<u8>,
}
pub(super) struct PooledUf(Option<UfStorage>);
impl Deref for PooledUf {
type Target = UfStorage;
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap()
}
}
impl DerefMut for PooledUf {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap()
}
}
impl Drop for PooledUf {
fn drop(&mut self) {
let storage = self.0.take().unwrap();
UF_POOL.with(|pool| pool.borrow_mut().push(storage));
}
}
pub(super) struct PooledCounts(Option<Vec<u32>>);
impl Deref for PooledCounts {
type Target = [u32];
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap()
}
}
impl DerefMut for PooledCounts {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap()
}
}
impl Drop for PooledCounts {
fn drop(&mut self) {
let counts = self.0.take().unwrap();
COUNTS_POOL.with(|pool| pool.borrow_mut().push(counts));
}
}
thread_local! {
static UF_POOL: RefCell<Vec<UfStorage>> = const { RefCell::new(Vec::new()) };
static COUNTS_POOL: RefCell<Vec<Vec<u32>>> = const { RefCell::new(Vec::new()) };
}
#[inline]
pub(super) fn find(parent: &mut [u32], mut x: u32) -> u32 {
while parent[x as usize] != x {
parent[x as usize] = parent[parent[x as usize] as usize];
x = parent[x as usize];
}
x
}
#[inline]
pub(super) fn union(parent: &mut [u32], rank: &mut [u8], x: u32, y: u32) {
let rx = find(parent, x);
let ry = find(parent, y);
if rx == ry {
return;
}
if rank[rx as usize] < rank[ry as usize] {
parent[rx as usize] = ry;
} else {
parent[ry as usize] = rx;
if rank[rx as usize] == rank[ry as usize] {
rank[rx as usize] += 1;
}
}
}
#[inline]
pub(super) fn find_seed(
n_spins: usize,
rng: &mut Xoshiro256StarStar,
mut eligible: impl FnMut(usize) -> bool,
) -> Option<usize> {
for _ in 0..64 {
let i = rng.gen_range(0..n_spins);
if eligible(i) {
return Some(i);
}
}
None
}
#[inline]
pub(super) fn dfs_cluster(
lattice: &Lattice,
seed: usize,
in_cluster: &mut [bool],
stack: &mut Vec<usize>,
mut should_add: impl FnMut(usize, usize, usize, bool) -> bool,
) {
in_cluster[seed] = true;
stack.push(seed);
while let Some(site) = stack.pop() {
for d in 0..lattice.n_neighbors {
let fwd = lattice.neighbor_fwd(site, d);
if !in_cluster[fwd] && should_add(site, fwd, d, true) {
in_cluster[fwd] = true;
stack.push(fwd);
}
let bwd = lattice.neighbor_bwd(site, d);
if !in_cluster[bwd] && should_add(site, bwd, d, false) {
in_cluster[bwd] = true;
stack.push(bwd);
}
}
}
}
#[inline]
pub(super) fn uf_bonds(
lattice: &Lattice,
should_bond: impl FnMut(usize, usize) -> bool,
) -> PooledUf {
uf_bonds_with(lattice, should_bond, |_site, _dim| {})
}
#[inline]
pub(super) fn uf_bonds_with(
lattice: &Lattice,
mut should_bond: impl FnMut(usize, usize) -> bool,
mut on_bond: impl FnMut(usize, usize),
) -> PooledUf {
let n_spins = lattice.n_spins;
let mut storage = UF_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_default();
reset_uf(&mut storage.parent, &mut storage.rank, n_spins);
activate_bonds(
&mut storage.parent,
&mut storage.rank,
lattice,
&mut should_bond,
&mut on_bond,
);
PooledUf(Some(storage))
}
#[inline]
pub(super) fn uf_bonds_fresh(
lattice: &Lattice,
should_bond: impl FnMut(usize, usize) -> bool,
) -> (Vec<u32>, Vec<u8>) {
uf_bonds_fresh_with(lattice, should_bond, |_site, _dim| {})
}
#[inline]
pub(super) fn uf_bonds_fresh_with(
lattice: &Lattice,
mut should_bond: impl FnMut(usize, usize) -> bool,
mut on_bond: impl FnMut(usize, usize),
) -> (Vec<u32>, Vec<u8>) {
let mut parent = Vec::new();
let mut rank = Vec::new();
reset_uf(&mut parent, &mut rank, lattice.n_spins);
activate_bonds(
&mut parent,
&mut rank,
lattice,
&mut should_bond,
&mut on_bond,
);
(parent, rank)
}
#[inline]
fn reset_uf(parent: &mut Vec<u32>, rank: &mut Vec<u8>, n_spins: usize) {
parent.resize(n_spins, 0);
for (i, parent) in parent.iter_mut().enumerate() {
*parent = i as u32;
}
rank.resize(n_spins, 0);
rank.fill(0);
}
#[inline]
fn activate_bonds(
parent: &mut [u32],
rank: &mut [u8],
lattice: &Lattice,
should_bond: &mut impl FnMut(usize, usize) -> bool,
on_bond: &mut impl FnMut(usize, usize),
) {
for i in 0..lattice.n_spins {
for d in 0..lattice.n_neighbors {
if should_bond(i, d) {
let j = lattice.neighbor_fwd(i, d);
union(parent, rank, i as u32, j as u32);
on_bond(i, d);
}
}
}
}
#[inline]
pub(super) fn uf_bonds_extend(
parent: &mut [u32],
rank: &mut [u8],
lattice: &Lattice,
mut should_bond: impl FnMut(usize, usize) -> bool,
) {
activate_bonds(parent, rank, lattice, &mut should_bond, &mut |_, _| {});
}
#[inline]
pub(super) fn build_graph(
lattice: &Lattice,
observe: bool,
should_bond: impl FnMut(usize, usize) -> bool,
) -> (PooledUf, Option<BondMetrics>) {
if !observe {
return (uf_bonds(lattice, should_bond), None);
}
let mut metrics = BondMetrics::new(lattice);
let uf = uf_bonds_with(lattice, should_bond, |site, dim| {
metrics.record_bond(lattice, site, dim);
});
(uf, Some(metrics))
}
#[inline]
pub(super) fn uf_flatten_counts(parent: &mut [u32]) -> PooledCounts {
let n = parent.len();
uf_flatten(parent);
let mut counts = COUNTS_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_default();
counts.resize(n, 0);
counts.fill(0);
count_roots(parent, &mut counts);
PooledCounts(Some(counts))
}
#[inline]
pub(super) fn uf_flatten_counts_fresh(parent: &mut [u32]) -> Vec<u32> {
let mut counts = vec![0u32; parent.len()];
uf_flatten(parent);
count_roots(parent, &mut counts);
counts
}
#[inline]
fn count_roots(parent: &[u32], counts: &mut [u32]) {
for &root in parent {
counts[root as usize] += 1;
}
}
#[inline]
pub(super) fn uf_flatten(parent: &mut [u32]) {
for i in 0..parent.len() {
parent[i] = find(parent, i as u32);
}
}
pub(super) const UNDECIDED: u8 = u8::MAX;
#[inline]
pub(super) fn decide_clusters(
parent: &[u32],
decisions: &mut [u8],
mut eligible: impl FnMut(usize, usize) -> bool,
mut decide: impl FnMut(usize) -> u8,
mut apply: impl FnMut(usize, u8),
) {
decisions.fill(UNDECIDED);
for (site, &root) in parent.iter().enumerate() {
let root = root as usize;
if !eligible(site, root) {
continue;
}
if decisions[root] == UNDECIDED {
decisions[root] = decide(root);
}
apply(site, decisions[root]);
}
}
#[inline]
pub(super) fn uf_histogram(sizes: impl IntoIterator<Item = u32>, hist: &mut [u64]) {
for size in sizes {
if size > 0 {
hist[size as usize] += 1;
}
}
}
pub(super) fn top4_sizes(sizes: impl IntoIterator<Item = u32>) -> [u32; 4] {
let mut top = [0u32; 4]; for c in sizes {
if c > top[0] {
top[0] = c;
top.sort_unstable();
}
}
top.reverse(); top
}
pub(crate) const MAX_WINDING_DIMS: usize = 3;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct GraphObservationSlot {
pub top4: [u32; 4],
pub active_bonds: u32,
pub wraps: [bool; MAX_WINDING_DIMS],
pub wraps_all: bool,
pub wrapping_clusters: u32,
pub wrapping_sites: u32,
pub large_components: u32,
pub s2: f64,
pub s4: f64,
pub observed: bool,
}
const LARGE_COMPONENT_FRACTION: f64 = 0.05;
enum Winding {
Untracked,
Square(WindingUf<2>),
Cubic(WindingUf<3>),
}
pub(super) struct BondMetrics {
active_bonds: u32,
winding: Winding,
}
impl BondMetrics {
pub fn new(lattice: &Lattice) -> Self {
let n_sites = lattice.n_spins;
let winding = if let Some((l0, l1)) = lattice.square_shape() {
Winding::Square(WindingUf::new(n_sites, [l0, l1]))
} else if let Some(shape) = lattice.cubic_shape() {
Winding::Cubic(WindingUf::new(n_sites, shape))
} else {
Winding::Untracked
};
Self {
active_bonds: 0,
winding,
}
}
#[inline]
pub fn record_bond(&mut self, lattice: &Lattice, site: usize, dim: usize) {
self.active_bonds += 1;
let neighbor = || lattice.neighbor_fwd(site, dim);
match &mut self.winding {
Winding::Untracked => {}
Winding::Square(winding) => winding.union_forward(site, neighbor(), dim),
Winding::Cubic(winding) => winding.union_forward(site, neighbor(), dim),
}
}
pub fn finish(
self,
n_sites: usize,
clusters: impl Iterator<Item = (usize, u32)> + Clone,
) -> GraphObservationSlot {
let threshold = ((n_sites as f64) * LARGE_COMPONENT_FRACTION).ceil() as u32;
let sizes = clusters.clone().map(|(_, size)| size);
let fraction = |size: u32| f64::from(size) / n_sites as f64;
let mut slot = GraphObservationSlot {
top4: top4_sizes(sizes.clone()),
active_bonds: self.active_bonds,
large_components: sizes.clone().filter(|&size| size >= threshold).count() as u32,
s2: sizes.clone().map(|size| fraction(size).powi(2)).sum(),
s4: sizes.map(|size| fraction(size).powi(4)).sum(),
observed: true,
..Default::default()
};
match &self.winding {
Winding::Untracked => {}
Winding::Square(winding) => winding.summarize(clusters, &mut slot),
Winding::Cubic(winding) => winding.summarize(clusters, &mut slot),
}
slot
}
}
struct WindingUf<const D: usize> {
parent: Vec<u32>,
rank: Vec<u8>,
displacement: Vec<[i64; D]>,
wrap: Vec<[bool; D]>,
shape: [i64; D],
}
impl<const D: usize> WindingUf<D> {
fn new(n_sites: usize, shape: [usize; D]) -> Self {
Self {
parent: (0..n_sites as u32).collect(),
rank: vec![0; n_sites],
displacement: vec![[0; D]; n_sites],
wrap: vec![[false; D]; n_sites],
shape: shape.map(|extent| extent as i64),
}
}
fn find(&mut self, site: usize) -> (usize, [i64; D]) {
let parent = self.parent[site] as usize;
if parent == site {
return (site, [0; D]);
}
let own = self.displacement[site];
let (root, parent_to_root) = self.find(parent);
let to_root = std::array::from_fn(|axis| own[axis] + parent_to_root[axis]);
self.parent[site] = root as u32;
self.displacement[site] = to_root;
(root, to_root)
}
fn union_forward(&mut self, site: usize, neighbor: usize, dim: usize) {
let (root_site, disp_site) = self.find(site);
let (root_neighbor, disp_neighbor) = self.find(neighbor);
let mut neighbor_from_site: [i64; D] =
std::array::from_fn(|axis| disp_site[axis] - disp_neighbor[axis]);
neighbor_from_site[dim] += 1;
if root_site == root_neighbor {
for (axis, &displacement) in neighbor_from_site.iter().enumerate() {
debug_assert_eq!(displacement % self.shape[axis], 0);
self.wrap[root_site][axis] |= displacement != 0;
}
return;
}
let combined_wrap: [bool; D] = std::array::from_fn(|axis| {
self.wrap[root_site][axis] || self.wrap[root_neighbor][axis]
});
if self.rank[root_site] < self.rank[root_neighbor] {
self.parent[root_site] = root_neighbor as u32;
self.displacement[root_site] = neighbor_from_site.map(|d| -d);
self.wrap[root_neighbor] = combined_wrap;
return;
}
self.parent[root_neighbor] = root_site as u32;
self.displacement[root_neighbor] = neighbor_from_site;
self.wrap[root_site] = combined_wrap;
if self.rank[root_site] == self.rank[root_neighbor] {
self.rank[root_site] += 1;
}
}
fn summarize(
&self,
clusters: impl Iterator<Item = (usize, u32)>,
slot: &mut GraphObservationSlot,
) {
for (root, size) in clusters.filter(|&(_, size)| size > 0) {
debug_assert_eq!(self.parent[root] as usize, root);
let wrap = self.wrap[root];
if !wrap.contains(&true) {
continue;
}
slot.wrapping_clusters += 1;
slot.wrapping_sites += size;
slot.wraps_all |= !wrap.contains(&false);
for (seen, &wraps) in slot.wraps.iter_mut().zip(&wrap) {
*seen |= wraps;
}
}
}
#[cfg(test)]
fn winding(&self) -> [bool; D] {
let mut seen = [false; D];
for (site, wrap) in self.wrap.iter().enumerate() {
if self.parent[site] as usize != site {
continue;
}
for (seen, &wraps) in seen.iter_mut().zip(wrap) {
*seen |= wraps;
}
}
seen
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn lattice_4x4() -> Lattice {
Lattice::new(vec![4, 4])
}
fn bond_set() -> HashSet<(usize, usize)> {
[(0, 0), (0, 1), (3, 1), (10, 0), (10, 1)]
.into_iter()
.collect()
}
fn dfs_bond_closure(
bonds: &HashSet<(usize, usize)>,
) -> impl FnMut(usize, usize, usize, bool) -> bool + '_ {
|site, nb, d, fwd| {
let src = if fwd { site } else { nb };
bonds.contains(&(src, d))
}
}
#[test]
fn test_dfs_bond_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let bonds = bond_set();
let mut in_cluster = vec![false; n];
let mut stack = Vec::new();
dfs_cluster(
&lattice,
0,
&mut in_cluster,
&mut stack,
dfs_bond_closure(&bonds),
);
let cluster: HashSet<usize> = (0..n).filter(|&i| in_cluster[i]).collect();
assert_eq!(cluster, [0, 1, 3, 4].into_iter().collect());
in_cluster.fill(false);
stack.clear();
dfs_cluster(
&lattice,
10,
&mut in_cluster,
&mut stack,
dfs_bond_closure(&bonds),
);
let cluster: HashSet<usize> = (0..n).filter(|&i| in_cluster[i]).collect();
assert_eq!(cluster, [10, 11, 14].into_iter().collect());
in_cluster.fill(false);
stack.clear();
dfs_cluster(
&lattice,
7,
&mut in_cluster,
&mut stack,
dfs_bond_closure(&bonds),
);
let cluster: HashSet<usize> = (0..n).filter(|&i| in_cluster[i]).collect();
assert_eq!(cluster, [7].into_iter().collect());
}
fn active_sites() -> HashSet<usize> {
[0, 3, 10].into_iter().collect()
}
fn dfs_site_closure(
sites: &HashSet<usize>,
) -> impl FnMut(usize, usize, usize, bool) -> bool + '_ {
|site, nb, _d, fwd| {
let src = if fwd { site } else { nb };
sites.contains(&src)
}
}
#[test]
fn test_dfs_site_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let sites = active_sites();
let mut in_cluster = vec![false; n];
let mut stack = Vec::new();
dfs_cluster(
&lattice,
0,
&mut in_cluster,
&mut stack,
dfs_site_closure(&sites),
);
let cluster: HashSet<usize> = (0..n).filter(|&i| in_cluster[i]).collect();
assert_eq!(cluster, [0, 1, 3, 4, 7].into_iter().collect());
in_cluster.fill(false);
stack.clear();
dfs_cluster(
&lattice,
10,
&mut in_cluster,
&mut stack,
dfs_site_closure(&sites),
);
let cluster: HashSet<usize> = (0..n).filter(|&i| in_cluster[i]).collect();
assert_eq!(cluster, [10, 11, 14].into_iter().collect());
}
#[test]
fn test_uf_bond_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let bonds = bond_set();
let mut uf = uf_bonds(&lattice, |i, d| bonds.contains(&(i, d)));
let parent = &mut uf.parent;
for i in 0..n {
let root = find(parent, i as u32);
parent[i] = root;
}
let root_a = parent[0];
for &s in &[0, 1, 3, 4] {
assert_eq!(parent[s], root_a, "site {s} should be in cluster A");
}
let root_b = parent[10];
for &s in &[10, 11, 14] {
assert_eq!(parent[s], root_b, "site {s} should be in cluster B");
}
assert_ne!(root_a, root_b);
let clustered: HashSet<usize> = [0, 1, 3, 4, 10, 11, 14].into_iter().collect();
for (i, &site_parent) in parent.iter().enumerate().take(n) {
if !clustered.contains(&i) {
assert_eq!(site_parent, i as u32, "site {i} should be a singleton");
}
}
}
#[test]
fn test_uf_site_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let sites = active_sites();
let mut uf = uf_bonds(&lattice, |i, _d| sites.contains(&i));
let parent = &mut uf.parent;
for i in 0..n {
let root = find(parent, i as u32);
parent[i] = root;
}
let root_a = parent[0];
for &s in &[0, 1, 3, 4, 7] {
assert_eq!(parent[s], root_a, "site {s} should be in cluster A");
}
let root_b = parent[10];
for &s in &[10, 11, 14] {
assert_eq!(parent[s], root_b, "site {s} should be in cluster B");
}
assert_ne!(root_a, root_b);
let clustered: HashSet<usize> = [0, 1, 3, 4, 7, 10, 11, 14].into_iter().collect();
for (i, &site_parent) in parent.iter().enumerate().take(n) {
if !clustered.contains(&i) {
assert_eq!(site_parent, i as u32, "site {i} should be a singleton");
}
}
}
#[test]
fn test_uf_csd_bond_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let bonds = bond_set();
let mut uf = uf_bonds(&lattice, |i, d| bonds.contains(&(i, d)));
let counts = uf_flatten_counts(&mut uf.parent);
let mut hist = vec![0u64; n + 1];
uf_histogram(counts.iter().copied(), &mut hist);
assert_eq!(hist[1], 9);
assert_eq!(hist[3], 1);
assert_eq!(hist[4], 1);
assert_eq!(hist.iter().sum::<u64>(), 11);
}
#[test]
fn test_uf_csd_site_based() {
let lattice = lattice_4x4();
let n = lattice.n_spins;
let sites = active_sites();
let mut uf = uf_bonds(&lattice, |i, _d| sites.contains(&i));
let counts = uf_flatten_counts(&mut uf.parent);
let mut hist = vec![0u64; n + 1];
uf_histogram(counts.iter().copied(), &mut hist);
assert_eq!(hist[1], 8);
assert_eq!(hist[3], 1);
assert_eq!(hist[5], 1);
assert_eq!(hist.iter().sum::<u64>(), 10);
}
#[test]
fn pooled_uf_resets_after_size_change() {
let larger = Lattice::new(vec![4, 4]);
{
let mut uf = uf_bonds(&larger, |_i, _d| true);
let counts = uf_flatten_counts(&mut uf.parent);
assert_eq!(counts.iter().sum::<u32>(), 16);
}
let smaller = Lattice::new(vec![3, 3]);
let mut uf = uf_bonds(&smaller, |_i, _d| false);
assert_eq!(uf.parent, (0..9).collect::<Vec<_>>());
assert!(uf.rank.iter().all(|&rank| rank == 0));
let counts = uf_flatten_counts(&mut uf.parent);
assert!(counts.iter().all(|&count| count == 1));
}
#[test]
fn winding_distinguishes_seams_and_noncontractible_cycles() {
let index = |row: usize, col: usize| 4 * row + col;
let mut seam = WindingUf::new(16, [4, 4]);
seam.union_forward(index(0, 3), index(0, 0), 1);
assert_eq!(seam.winding(), [false, false]);
let mut row = WindingUf::new(16, [4, 4]);
for col in 0..4 {
row.union_forward(index(1, col), index(1, (col + 1) % 4), 1);
}
assert_eq!(row.winding(), [false, true]);
let mut both = WindingUf::new(16, [4, 4]);
for col in 0..4 {
both.union_forward(index(0, col), index(0, (col + 1) % 4), 1);
}
for row in 0..4 {
both.union_forward(index(row, 0), index((row + 1) % 4, 0), 0);
}
assert_eq!(both.winding(), [true, true]);
}
#[test]
fn bond_metrics_reuse_component_counts() {
let lattice = lattice_4x4();
let bonds = bond_set();
let mut metrics = BondMetrics::new(&lattice);
let mut uf = uf_bonds_with(
&lattice,
|site, dim| bonds.contains(&(site, dim)),
|site, dim| metrics.record_bond(&lattice, site, dim),
);
let counts = uf_flatten_counts(&mut uf.parent);
let summary = metrics.finish(counts.len(), counts.iter().copied().enumerate());
assert_eq!(summary.active_bonds, bonds.len() as u32);
assert_eq!(summary.top4, [4, 3, 1, 1]);
assert!(summary.observed);
let sizes: Vec<f64> = counts
.iter()
.filter(|&&c| c > 0)
.map(|&c| c as f64 / 16.0)
.collect();
assert!((summary.s2 - sizes.iter().map(|s| s * s).sum::<f64>()).abs() < 1e-15);
assert!((summary.s4 - sizes.iter().map(|s| s.powi(4)).sum::<f64>()).abs() < 1e-15);
}
#[test]
fn wrapping_clusters_are_counted_with_their_mass() {
let wrap_of = |lattice: &Lattice, bonds: &[(usize, usize)]| {
let mut metrics = BondMetrics::new(lattice);
let mut uf = uf_bonds_with(
lattice,
|site, dim| bonds.contains(&(site, dim)),
|site, dim| metrics.record_bond(lattice, site, dim),
);
let counts = uf_flatten_counts(&mut uf.parent);
metrics.finish(counts.len(), counts.iter().copied().enumerate())
};
let square = lattice_4x4();
let row: Vec<(usize, usize)> = (4..8).map(|site| (site, 1)).chain([(12, 1)]).collect();
let slot = wrap_of(&square, &row);
assert_eq!(slot.wraps, [false, true, false]);
assert!(!slot.wraps_all);
assert_eq!((slot.wrapping_clusters, slot.wrapping_sites), (1, 4));
let cubic = Lattice::new(vec![3, 3, 3]);
let line: Vec<(usize, usize)> = (0..3).map(|x| (9 * x, 0)).collect();
let slot = wrap_of(&cubic, &line);
assert_eq!(slot.wraps, [true, false, false]);
assert_eq!((slot.wrapping_clusters, slot.wrapping_sites), (1, 3));
let all: Vec<(usize, usize)> = (0..27).flat_map(|i| (0..3).map(move |d| (i, d))).collect();
let slot = wrap_of(&cubic, &all);
assert!(slot.wraps_all);
assert_eq!((slot.wrapping_clusters, slot.wrapping_sites), (1, 27));
assert!((slot.s2 - 1.0).abs() < 1e-15);
}
}