use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Particle {
pub pos: [f64; 3],
pub vel: [f64; 3],
pub force: [f64; 3],
pub mass: f64,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum CacheLayoutError {
#[error("index {index} out of bounds for container of length {len}")]
IndexOutOfBounds {
index: usize,
len: usize,
},
#[error("swap indices must be distinct, but both are {index}")]
IdenticalSwapIndices {
index: usize,
},
#[error("grid spacing must be positive and finite, got {value}")]
InvalidGridSpacing {
value: f64,
},
#[error("container is empty")]
EmptyContainer,
}
#[inline]
fn spread_bits_by_3(v: u32) -> u64 {
let mut x = u64::from(v & 0x001f_ffff);
x = (x | (x << 32)) & 0x001f_0000_0000_ffff;
x = (x | (x << 16)) & 0x001f_0000_ff00_00ff;
x = (x | (x << 8)) & 0x100f_00f0_0f00_f00f;
x = (x | (x << 4)) & 0x10c3_0c30_c30c_30c3;
x = (x | (x << 2)) & 0x1249_2492_4924_9249;
x
}
#[inline]
fn compact_bits_by_3(v: u64) -> u32 {
let mut x = v & 0x1249_2492_4924_9249;
x = (x | (x >> 2)) & 0x10c3_0c30_c30c_30c3;
x = (x | (x >> 4)) & 0x100f_00f0_0f00_f00f;
x = (x | (x >> 8)) & 0x001f_0000_ff00_00ff;
x = (x | (x >> 16)) & 0x001f_0000_0000_ffff;
x = (x | (x >> 32)) & 0x001f_ffff;
x as u32
}
#[inline]
#[must_use]
pub fn morton_encode_3d(ix: u32, iy: u32, iz: u32) -> u64 {
spread_bits_by_3(ix) | (spread_bits_by_3(iy) << 1) | (spread_bits_by_3(iz) << 2)
}
#[inline]
#[must_use]
pub fn morton_decode_3d(code: u64) -> (u32, u32, u32) {
(
compact_bits_by_3(code),
compact_bits_by_3(code >> 1),
compact_bits_by_3(code >> 2),
)
}
pub fn position_to_morton(pos: [f64; 3], grid_spacing: f64) -> Result<u64, CacheLayoutError> {
if !grid_spacing.is_finite() || grid_spacing <= 0.0 {
return Err(CacheLayoutError::InvalidGridSpacing {
value: grid_spacing,
});
}
let max_cell: u32 = (1u32 << 21) - 1; let inv = 1.0 / grid_spacing;
let quantise = |v: f64| -> u32 {
let q = (v * inv).floor();
if q < 0.0 {
0
} else if q > f64::from(max_cell) {
max_cell
} else {
q as u32
}
};
let ix = quantise(pos[0]);
let iy = quantise(pos[1]);
let iz = quantise(pos[2]);
Ok(morton_encode_3d(ix, iy, iz))
}
#[derive(Clone)]
pub struct AlignedVec<T> {
data: Vec<T>,
}
impl<T: Default + Clone> AlignedVec<T> {
#[must_use]
pub fn new(len: usize) -> Self {
Self {
data: vec![T::default(); len],
}
}
}
impl<T: Clone> AlignedVec<T> {
#[must_use]
pub fn from_vec(v: Vec<T>) -> Self {
Self { data: v }
}
#[must_use]
pub fn as_slice(&self) -> &[T] {
&self.data
}
#[must_use]
pub fn as_mut_slice(&mut self) -> &mut [T] {
&mut self.data
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
impl<T: fmt::Debug + Clone> fmt::Debug for AlignedVec<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AlignedVec")
.field("len", &self.data.len())
.field("data", &self.data)
.finish()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParticleSoA {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub z: Vec<f64>,
pub vx: Vec<f64>,
pub vy: Vec<f64>,
pub vz: Vec<f64>,
pub fx: Vec<f64>,
pub fy: Vec<f64>,
pub fz: Vec<f64>,
pub mass: Vec<f64>,
len: usize,
}
impl ParticleSoA {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
x: Vec::with_capacity(capacity),
y: Vec::with_capacity(capacity),
z: Vec::with_capacity(capacity),
vx: Vec::with_capacity(capacity),
vy: Vec::with_capacity(capacity),
vz: Vec::with_capacity(capacity),
fx: Vec::with_capacity(capacity),
fy: Vec::with_capacity(capacity),
fz: Vec::with_capacity(capacity),
mass: Vec::with_capacity(capacity),
len: 0,
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.x.capacity()
}
pub fn push(&mut self, pos: [f64; 3], vel: [f64; 3], force: [f64; 3], mass: f64) {
self.x.push(pos[0]);
self.y.push(pos[1]);
self.z.push(pos[2]);
self.vx.push(vel[0]);
self.vy.push(vel[1]);
self.vz.push(vel[2]);
self.fx.push(force[0]);
self.fy.push(force[1]);
self.fz.push(force[2]);
self.mass.push(mass);
self.len += 1;
}
pub fn get_position(&self, i: usize) -> Result<[f64; 3], CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
Ok([self.x[i], self.y[i], self.z[i]])
}
pub fn set_position(&mut self, i: usize, pos: [f64; 3]) -> Result<(), CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
self.x[i] = pos[0];
self.y[i] = pos[1];
self.z[i] = pos[2];
Ok(())
}
pub fn get_velocity(&self, i: usize) -> Result<[f64; 3], CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
Ok([self.vx[i], self.vy[i], self.vz[i]])
}
pub fn set_velocity(&mut self, i: usize, vel: [f64; 3]) -> Result<(), CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
self.vx[i] = vel[0];
self.vy[i] = vel[1];
self.vz[i] = vel[2];
Ok(())
}
pub fn get_force(&self, i: usize) -> Result<[f64; 3], CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
Ok([self.fx[i], self.fy[i], self.fz[i]])
}
pub fn set_force(&mut self, i: usize, force: [f64; 3]) -> Result<(), CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
self.fx[i] = force[0];
self.fy[i] = force[1];
self.fz[i] = force[2];
Ok(())
}
pub fn zero_forces(&mut self) {
self.fx.fill(0.0);
self.fy.fill(0.0);
self.fz.fill(0.0);
}
#[must_use]
pub fn from_aos(particles: &[Particle]) -> Self {
let n = particles.len();
let mut soa = Self::new(n);
for p in particles {
soa.push(p.pos, p.vel, p.force, p.mass);
}
soa
}
#[must_use]
pub fn to_aos(&self) -> Vec<Particle> {
let mut out = Vec::with_capacity(self.len);
for i in 0..self.len {
out.push(Particle {
pos: [self.x[i], self.y[i], self.z[i]],
vel: [self.vx[i], self.vy[i], self.vz[i]],
force: [self.fx[i], self.fy[i], self.fz[i]],
mass: self.mass[i],
});
}
out
}
pub fn swap(&mut self, i: usize, j: usize) -> Result<(), CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
if j >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: j,
len: self.len,
});
}
if i == j {
return Err(CacheLayoutError::IdenticalSwapIndices { index: i });
}
self.x.swap(i, j);
self.y.swap(i, j);
self.z.swap(i, j);
self.vx.swap(i, j);
self.vy.swap(i, j);
self.vz.swap(i, j);
self.fx.swap(i, j);
self.fy.swap(i, j);
self.fz.swap(i, j);
self.mass.swap(i, j);
Ok(())
}
pub fn sort_by_morton_code(&mut self, grid_spacing: f64) -> Result<(), CacheLayoutError> {
if !grid_spacing.is_finite() || grid_spacing <= 0.0 {
return Err(CacheLayoutError::InvalidGridSpacing {
value: grid_spacing,
});
}
if self.len < 2 {
return Ok(());
}
let mut indices: Vec<(u64, usize)> = Vec::with_capacity(self.len);
for i in 0..self.len {
let code = position_to_morton([self.x[i], self.y[i], self.z[i]], grid_spacing)?;
indices.push((code, i));
}
indices.sort_unstable_by_key(|&(code, _)| code);
let mut new_x = Vec::with_capacity(self.len);
let mut new_y = Vec::with_capacity(self.len);
let mut new_z = Vec::with_capacity(self.len);
let mut new_vx = Vec::with_capacity(self.len);
let mut new_vy = Vec::with_capacity(self.len);
let mut new_vz = Vec::with_capacity(self.len);
let mut new_fx = Vec::with_capacity(self.len);
let mut new_fy = Vec::with_capacity(self.len);
let mut new_fz = Vec::with_capacity(self.len);
let mut new_mass = Vec::with_capacity(self.len);
for &(_, idx) in &indices {
new_x.push(self.x[idx]);
new_y.push(self.y[idx]);
new_z.push(self.z[idx]);
new_vx.push(self.vx[idx]);
new_vy.push(self.vy[idx]);
new_vz.push(self.vz[idx]);
new_fx.push(self.fx[idx]);
new_fy.push(self.fy[idx]);
new_fz.push(self.fz[idx]);
new_mass.push(self.mass[idx]);
}
self.x = new_x;
self.y = new_y;
self.z = new_z;
self.vx = new_vx;
self.vy = new_vy;
self.vz = new_vz;
self.fx = new_fx;
self.fy = new_fy;
self.fz = new_fz;
self.mass = new_mass;
Ok(())
}
#[inline]
pub fn prefetch_range(&self, _start: usize, _end: usize) {
}
pub fn get_mass(&self, i: usize) -> Result<f64, CacheLayoutError> {
if i >= self.len {
return Err(CacheLayoutError::IndexOutOfBounds {
index: i,
len: self.len,
});
}
Ok(self.mass[i])
}
pub fn pop(&mut self) -> Result<Particle, CacheLayoutError> {
if self.len == 0 {
return Err(CacheLayoutError::EmptyContainer);
}
self.len -= 1;
let px = self.x.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let py = self.y.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pz = self.z.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pvx = self.vx.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pvy = self.vy.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pvz = self.vz.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pfx = self.fx.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pfy = self.fy.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pfz = self.fz.pop().ok_or(CacheLayoutError::EmptyContainer)?;
let pm = self.mass.pop().ok_or(CacheLayoutError::EmptyContainer)?;
Ok(Particle {
pos: [px, py, pz],
vel: [pvx, pvy, pvz],
force: [pfx, pfy, pfz],
mass: pm,
})
}
pub fn clear(&mut self) {
self.x.clear();
self.y.clear();
self.z.clear();
self.vx.clear();
self.vy.clear();
self.vz.clear();
self.fx.clear();
self.fy.clear();
self.fz.clear();
self.mass.clear();
self.len = 0;
}
pub fn reserve(&mut self, additional: usize) {
self.x.reserve(additional);
self.y.reserve(additional);
self.z.reserve(additional);
self.vx.reserve(additional);
self.vy.reserve(additional);
self.vz.reserve(additional);
self.fx.reserve(additional);
self.fy.reserve(additional);
self.fz.reserve(additional);
self.mass.reserve(additional);
}
pub fn bounding_box(&self) -> Result<([f64; 3], [f64; 3]), CacheLayoutError> {
if self.len == 0 {
return Err(CacheLayoutError::EmptyContainer);
}
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
for i in 0..self.len {
if self.x[i] < min[0] {
min[0] = self.x[i];
}
if self.x[i] > max[0] {
max[0] = self.x[i];
}
if self.y[i] < min[1] {
min[1] = self.y[i];
}
if self.y[i] > max[1] {
max[1] = self.y[i];
}
if self.z[i] < min[2] {
min[2] = self.z[i];
}
if self.z[i] > max[2] {
max[2] = self.z[i];
}
}
Ok((min, max))
}
#[must_use]
pub fn kinetic_energy(&self) -> f64 {
let mut ke = 0.0;
for i in 0..self.len {
let v2 = self.vx[i] * self.vx[i] + self.vy[i] * self.vy[i] + self.vz[i] * self.vz[i];
ke += 0.5 * self.mass[i] * v2;
}
ke
}
pub fn centre_of_mass(&self) -> Result<[f64; 3], CacheLayoutError> {
if self.len == 0 {
return Err(CacheLayoutError::EmptyContainer);
}
let mut total_mass = 0.0;
let mut cx = 0.0;
let mut cy = 0.0;
let mut cz = 0.0;
for i in 0..self.len {
let m = self.mass[i];
cx += m * self.x[i];
cy += m * self.y[i];
cz += m * self.z[i];
total_mass += m;
}
if total_mass.abs() < f64::EPSILON {
return Err(CacheLayoutError::EmptyContainer);
}
let inv = 1.0 / total_mass;
Ok([cx * inv, cy * inv, cz * inv])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn morton_encode_decode_round_trip() {
let cases: Vec<(u32, u32, u32)> = vec![
(0, 0, 0),
(1, 0, 0),
(0, 1, 0),
(0, 0, 1),
(1, 1, 1),
(7, 13, 21),
(255, 255, 255),
(1023, 512, 768),
((1 << 21) - 1, (1 << 21) - 1, (1 << 21) - 1),
];
for (ix, iy, iz) in cases {
let code = morton_encode_3d(ix, iy, iz);
let (dx, dy, dz) = morton_decode_3d(code);
assert_eq!(
(dx, dy, dz),
(ix, iy, iz),
"round-trip failed for ({ix}, {iy}, {iz})"
);
}
}
#[test]
fn morton_ordering_preserves_locality() {
let c1 = morton_encode_3d(4, 4, 4);
let c2 = morton_encode_3d(5, 4, 4);
let c_far = morton_encode_3d(100, 100, 100);
let diff_near = c1.abs_diff(c2);
let diff_far = c1.abs_diff(c_far);
assert!(diff_near < diff_far);
}
#[test]
fn position_to_morton_basic() {
let code = position_to_morton([1.5, 2.5, 3.5], 1.0);
assert!(code.is_ok());
let code = code.expect("should succeed");
let (ix, iy, iz) = morton_decode_3d(code);
assert_eq!((ix, iy, iz), (1, 2, 3));
}
#[test]
fn position_to_morton_rejects_invalid_spacing() {
assert!(position_to_morton([0.0, 0.0, 0.0], 0.0).is_err());
assert!(position_to_morton([0.0, 0.0, 0.0], -1.0).is_err());
assert!(position_to_morton([0.0, 0.0, 0.0], f64::NAN).is_err());
assert!(position_to_morton([0.0, 0.0, 0.0], f64::INFINITY).is_err());
}
#[test]
fn position_to_morton_clamps_negative_coords() {
let code = position_to_morton([-5.0, -10.0, -1.0], 1.0);
assert!(code.is_ok());
let (ix, iy, iz) = morton_decode_3d(code.expect("should succeed"));
assert_eq!((ix, iy, iz), (0, 0, 0));
}
#[test]
fn soa_new_and_push() {
let mut soa = ParticleSoA::new(8);
assert!(soa.is_empty());
assert_eq!(soa.len(), 0);
assert!(soa.capacity() >= 8);
soa.push([1.0, 2.0, 3.0], [0.1, 0.2, 0.3], [10.0, 20.0, 30.0], 5.0);
assert_eq!(soa.len(), 1);
assert!(!soa.is_empty());
}
#[test]
fn soa_get_set_position() {
let mut soa = ParticleSoA::new(0);
soa.push([1.0, 2.0, 3.0], [0.0; 3], [0.0; 3], 1.0);
assert_eq!(soa.get_position(0), Ok([1.0, 2.0, 3.0]));
assert!(soa.set_position(0, [4.0, 5.0, 6.0]).is_ok());
assert_eq!(soa.get_position(0), Ok([4.0, 5.0, 6.0]));
assert!(soa.get_position(1).is_err());
assert!(soa.set_position(1, [0.0; 3]).is_err());
}
#[test]
fn soa_get_set_velocity() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [1.0, 2.0, 3.0], [0.0; 3], 1.0);
assert_eq!(soa.get_velocity(0), Ok([1.0, 2.0, 3.0]));
assert!(soa.set_velocity(0, [7.0, 8.0, 9.0]).is_ok());
assert_eq!(soa.get_velocity(0), Ok([7.0, 8.0, 9.0]));
assert!(soa.get_velocity(5).is_err());
}
#[test]
fn soa_get_set_force() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [0.0; 3], [10.0, 20.0, 30.0], 1.0);
assert_eq!(soa.get_force(0), Ok([10.0, 20.0, 30.0]));
assert!(soa.set_force(0, [40.0, 50.0, 60.0]).is_ok());
assert_eq!(soa.get_force(0), Ok([40.0, 50.0, 60.0]));
assert!(soa.get_force(1).is_err());
}
#[test]
fn soa_zero_forces() {
let mut soa = ParticleSoA::new(0);
for i in 0..100 {
let v = i as f64;
soa.push([v; 3], [v; 3], [v * 10.0; 3], 1.0);
}
soa.zero_forces();
for i in 0..100 {
assert_eq!(soa.get_force(i), Ok([0.0, 0.0, 0.0]));
let v = i as f64;
assert_eq!(soa.get_position(i), Ok([v, v, v]));
assert_eq!(soa.get_velocity(i), Ok([v, v, v]));
}
}
#[test]
fn soa_aos_round_trip() {
let particles = vec![
Particle {
pos: [1.0, 2.0, 3.0],
vel: [0.1, 0.2, 0.3],
force: [10.0, 20.0, 30.0],
mass: 1.5,
},
Particle {
pos: [4.0, 5.0, 6.0],
vel: [0.4, 0.5, 0.6],
force: [40.0, 50.0, 60.0],
mass: 2.5,
},
Particle {
pos: [7.0, 8.0, 9.0],
vel: [0.7, 0.8, 0.9],
force: [70.0, 80.0, 90.0],
mass: 3.5,
},
];
let soa = ParticleSoA::from_aos(&particles);
assert_eq!(soa.len(), 3);
let reconstructed = soa.to_aos();
assert_eq!(reconstructed, particles);
}
#[test]
fn soa_swap_preserves_data() {
let particles = vec![
Particle {
pos: [1.0, 2.0, 3.0],
vel: [0.1, 0.2, 0.3],
force: [10.0, 20.0, 30.0],
mass: 1.0,
},
Particle {
pos: [4.0, 5.0, 6.0],
vel: [0.4, 0.5, 0.6],
force: [40.0, 50.0, 60.0],
mass: 2.0,
},
];
let mut soa = ParticleSoA::from_aos(&particles);
assert!(soa.swap(0, 1).is_ok());
assert_eq!(soa.get_position(0), Ok([4.0, 5.0, 6.0]));
assert_eq!(soa.get_position(1), Ok([1.0, 2.0, 3.0]));
assert_eq!(soa.get_velocity(0), Ok([0.4, 0.5, 0.6]));
assert_eq!(soa.get_mass(0), Ok(2.0));
assert_eq!(soa.get_mass(1), Ok(1.0));
}
#[test]
fn soa_swap_identical_indices_errors() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [0.0; 3], [0.0; 3], 1.0);
assert!(soa.swap(0, 0).is_err());
}
#[test]
fn soa_swap_out_of_bounds_errors() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [0.0; 3], [0.0; 3], 1.0);
assert!(soa.swap(0, 5).is_err());
assert!(soa.swap(5, 0).is_err());
}
#[test]
fn soa_sort_by_morton_preserves_particle_data() {
let particles = vec![
Particle {
pos: [100.0, 100.0, 100.0],
vel: [0.0; 3],
force: [0.0; 3],
mass: 1.0,
},
Particle {
pos: [0.0, 0.0, 0.0],
vel: [1.0; 3],
force: [2.0; 3],
mass: 2.0,
},
Particle {
pos: [50.0, 50.0, 50.0],
vel: [3.0; 3],
force: [4.0; 3],
mass: 3.0,
},
];
let mut soa = ParticleSoA::from_aos(&particles);
assert!(soa.sort_by_morton_code(1.0).is_ok());
let sorted_aos = soa.to_aos();
assert_eq!(sorted_aos.len(), 3);
for p in &particles {
let count = sorted_aos.iter().filter(|s| *s == p).count();
assert_eq!(count, 1, "particle {p:?} should appear exactly once");
}
assert_eq!(sorted_aos[0].pos, [0.0, 0.0, 0.0]);
}
#[test]
fn soa_sort_by_morton_rejects_invalid_spacing() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [0.0; 3], [0.0; 3], 1.0);
assert!(soa.sort_by_morton_code(0.0).is_err());
assert!(soa.sort_by_morton_code(-1.0).is_err());
}
#[test]
fn soa_sort_by_morton_empty_and_single() {
let mut soa = ParticleSoA::new(0);
assert!(soa.sort_by_morton_code(1.0).is_ok());
soa.push([5.0, 5.0, 5.0], [0.0; 3], [0.0; 3], 1.0);
assert!(soa.sort_by_morton_code(1.0).is_ok());
assert_eq!(soa.get_position(0), Ok([5.0, 5.0, 5.0]));
}
#[test]
fn soa_capacity_grows() {
let mut soa = ParticleSoA::new(2);
assert!(soa.capacity() >= 2);
for i in 0..100 {
soa.push([i as f64; 3], [0.0; 3], [0.0; 3], 1.0);
}
assert_eq!(soa.len(), 100);
assert!(soa.capacity() >= 100);
}
#[test]
fn soa_reserve() {
let mut soa = ParticleSoA::new(0);
soa.reserve(1000);
assert!(soa.capacity() >= 1000);
assert_eq!(soa.len(), 0);
}
#[test]
fn soa_pop() {
let mut soa = ParticleSoA::new(0);
soa.push([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], 10.0);
let p = soa.pop();
assert!(p.is_ok());
let p = p.expect("pop should succeed");
assert_eq!(p.pos, [1.0, 2.0, 3.0]);
assert_eq!(p.vel, [4.0, 5.0, 6.0]);
assert_eq!(p.force, [7.0, 8.0, 9.0]);
assert_eq!(p.mass, 10.0);
assert!(soa.is_empty());
assert!(soa.pop().is_err());
}
#[test]
fn soa_clear() {
let mut soa = ParticleSoA::new(0);
for i in 0..50 {
soa.push([i as f64; 3], [0.0; 3], [0.0; 3], 1.0);
}
assert_eq!(soa.len(), 50);
soa.clear();
assert!(soa.is_empty());
assert_eq!(soa.len(), 0);
}
#[test]
fn soa_bounding_box() {
let mut soa = ParticleSoA::new(0);
soa.push([1.0, -2.0, 3.0], [0.0; 3], [0.0; 3], 1.0);
soa.push([4.0, 5.0, -6.0], [0.0; 3], [0.0; 3], 1.0);
soa.push([-7.0, 8.0, 9.0], [0.0; 3], [0.0; 3], 1.0);
let (min, max) = soa.bounding_box().expect("should succeed");
assert_eq!(min, [-7.0, -2.0, -6.0]);
assert_eq!(max, [4.0, 8.0, 9.0]);
}
#[test]
fn soa_bounding_box_empty_errors() {
let soa = ParticleSoA::new(0);
assert!(soa.bounding_box().is_err());
}
#[test]
fn soa_kinetic_energy() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0; 3], [3.0, 0.0, 0.0], [0.0; 3], 2.0);
soa.push([0.0; 3], [0.0, 4.0, 0.0], [0.0; 3], 1.0);
let ke = soa.kinetic_energy();
assert!((ke - 17.0).abs() < 1e-12);
}
#[test]
fn soa_centre_of_mass() {
let mut soa = ParticleSoA::new(0);
soa.push([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 1.0);
soa.push([10.0, 0.0, 0.0], [0.0; 3], [0.0; 3], 1.0);
let com = soa.centre_of_mass().expect("should succeed");
assert!((com[0] - 5.0).abs() < 1e-12);
assert!((com[1]).abs() < 1e-12);
assert!((com[2]).abs() < 1e-12);
}
#[test]
fn soa_centre_of_mass_empty_errors() {
let soa = ParticleSoA::new(0);
assert!(soa.centre_of_mass().is_err());
}
#[test]
fn aligned_vec_basic() {
let av: AlignedVec<f64> = AlignedVec::new(100);
assert_eq!(av.len(), 100);
assert!(!av.is_empty());
assert_eq!(av.as_slice()[0], 0.0);
let av2: AlignedVec<f64> = AlignedVec::from_vec(vec![1.0, 2.0, 3.0]);
assert_eq!(av2.len(), 3);
assert_eq!(av2.as_slice(), &[1.0, 2.0, 3.0]);
}
#[test]
fn aligned_vec_mut() {
let mut av: AlignedVec<f64> = AlignedVec::new(5);
av.as_mut_slice()[0] = 42.0;
assert_eq!(av.as_slice()[0], 42.0);
}
#[test]
fn aligned_vec_empty() {
let av: AlignedVec<f64> = AlignedVec::new(0);
assert!(av.is_empty());
assert_eq!(av.len(), 0);
}
#[test]
fn soa_push_get_consistency() {
let mut soa = ParticleSoA::new(0);
let n = 200;
for i in 0..n {
let v = i as f64;
soa.push(
[v, v + 1.0, v + 2.0],
[v * 0.1, v * 0.2, v * 0.3],
[v * 10.0, v * 20.0, v * 30.0],
v + 100.0,
);
}
assert_eq!(soa.len(), n);
for i in 0..n {
let v = i as f64;
assert_eq!(soa.get_position(i), Ok([v, v + 1.0, v + 2.0]));
assert_eq!(soa.get_velocity(i), Ok([v * 0.1, v * 0.2, v * 0.3]));
assert_eq!(soa.get_force(i), Ok([v * 10.0, v * 20.0, v * 30.0]));
assert_eq!(soa.get_mass(i), Ok(v + 100.0));
}
}
#[test]
fn soa_prefetch_range_no_panic() {
let mut soa = ParticleSoA::new(0);
for i in 0..10 {
soa.push([i as f64; 3], [0.0; 3], [0.0; 3], 1.0);
}
soa.prefetch_range(0, 10);
soa.prefetch_range(5, 5);
soa.prefetch_range(0, 0);
}
#[test]
fn soa_sort_by_morton_larger_set() {
let mut soa = ParticleSoA::new(0);
for i in (0..50).rev() {
let v = i as f64 * 2.0;
soa.push([v, v, v], [0.0; 3], [0.0; 3], 1.0);
}
assert!(soa.sort_by_morton_code(1.0).is_ok());
assert_eq!(soa.len(), 50);
let mut prev_code = 0u64;
for i in 0..soa.len() {
let pos = soa.get_position(i).expect("valid index");
let code = position_to_morton(pos, 1.0).expect("valid");
assert!(code >= prev_code, "Morton order violated at index {i}");
prev_code = code;
}
}
}