1use rayon::prelude::*;
6
7use super::types::GpuCellList;
8
9pub(super) fn expand_bits(mut x: u32) -> u32 {
13 x &= 0x000003FF;
14 x = (x | (x << 16)) & 0x030000FF;
15 x = (x | (x << 8)) & 0x0300F00F;
16 x = (x | (x << 4)) & 0x030C30C3;
17 x = (x | (x << 2)) & 0x09249249;
18 x
19}
20pub fn morton_encode(x: u32, y: u32, z: u32) -> u32 {
24 expand_bits(x) | (expand_bits(y) << 1) | (expand_bits(z) << 2)
25}
26pub fn morton_decode(code: u32) -> (u32, u32, u32) {
28 (
29 compact_bits(code),
30 compact_bits(code >> 1),
31 compact_bits(code >> 2),
32 )
33}
34pub(super) fn compact_bits(mut x: u32) -> u32 {
36 x &= 0x09249249;
37 x = (x | (x >> 2)) & 0x030C30C3;
38 x = (x | (x >> 4)) & 0x0300F00F;
39 x = (x | (x >> 8)) & 0x030000FF;
40 x = (x | (x >> 16)) & 0x000003FF;
41 x
42}
43pub fn morton_sort(
52 positions: &[[f64; 3]],
53 box_min: [f64; 3],
54 box_max: [f64; 3],
55) -> (Vec<usize>, Vec<u32>) {
56 let range = [
57 (box_max[0] - box_min[0]).max(1e-10),
58 (box_max[1] - box_min[1]).max(1e-10),
59 (box_max[2] - box_min[2]).max(1e-10),
60 ];
61 let mut codes: Vec<(u32, usize)> = positions
62 .par_iter()
63 .enumerate()
64 .map(|(i, p)| {
65 let x = (((p[0] - box_min[0]) / range[0] * 1023.0) as u32).min(1023);
66 let y = (((p[1] - box_min[1]) / range[1] * 1023.0) as u32).min(1023);
67 let z = (((p[2] - box_min[2]) / range[2] * 1023.0) as u32).min(1023);
68 (morton_encode(x, y, z), i)
69 })
70 .collect();
71 codes.sort_by_key(|&(code, _)| code);
72 let sorted_indices: Vec<usize> = codes.iter().map(|&(_, idx)| idx).collect();
73 let morton_codes: Vec<u32> = codes.iter().map(|&(code, _)| code).collect();
74 (sorted_indices, morton_codes)
75}
76pub fn parallel_prefix_sum(counts: &[usize]) -> Vec<usize> {
78 let mut out = Vec::with_capacity(counts.len());
79 let mut acc = 0usize;
80 for &c in counts {
81 out.push(acc);
82 acc += c;
83 }
84 out
85}
86pub fn compute_bounding_box(positions: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
90 if positions.is_empty() {
91 return ([0.0; 3], [0.0; 3]);
92 }
93 let mut min = positions[0];
94 let mut max = positions[0];
95 for p in positions {
96 for d in 0..3 {
97 if p[d] < min[d] {
98 min[d] = p[d];
99 }
100 if p[d] > max[d] {
101 max[d] = p[d];
102 }
103 }
104 }
105 (min, max)
106}
107pub fn reorder_by_permutation<T: Clone>(data: &[T], perm: &[usize]) -> Vec<T> {
111 perm.iter().map(|&i| data[i].clone()).collect()
112}
113pub fn radix_sort_mock(keys: &[u32]) -> (Vec<u32>, Vec<usize>) {
118 if keys.is_empty() {
119 return (vec![], vec![]);
120 }
121 let mut indexed: Vec<(u32, usize)> = keys.iter().copied().zip(0..).collect();
122 indexed.sort_by_key(|&(k, _)| k);
123 let sorted_keys: Vec<u32> = indexed.iter().map(|&(k, _)| k).collect();
124 let sorted_indices: Vec<usize> = indexed.iter().map(|&(_, i)| i).collect();
125 (sorted_keys, sorted_indices)
126}
127pub fn gpu_prefix_sum(counts: &[usize]) -> Vec<usize> {
132 let mut out = Vec::with_capacity(counts.len());
133 let mut running = 0usize;
134 for &c in counts {
135 out.push(running);
136 running += c;
137 }
138 out
139}
140pub fn parallel_count_particles(
148 positions: &[[f64; 3]],
149 n_cells: [usize; 3],
150 cell_size: f64,
151) -> Vec<usize> {
152 let [nx, ny, nz] = n_cells;
153 let total = nx * ny * nz;
154 let mut counts = vec![0usize; total];
155 for p in positions {
156 let ix = ((p[0] / cell_size) as isize).clamp(0, nx as isize - 1) as usize;
157 let iy = ((p[1] / cell_size) as isize).clamp(0, ny as isize - 1) as usize;
158 let iz = ((p[2] / cell_size) as isize).clamp(0, nz as isize - 1) as usize;
159 counts[ix + nx * (iy + ny * iz)] += 1;
160 }
161 counts
162}
163pub fn distribute_cells_to_gpus(n_cells: usize, n_gpus: usize) -> Vec<std::ops::Range<usize>> {
168 if n_gpus == 0 || n_cells == 0 {
169 return vec![];
170 }
171 let base = n_cells / n_gpus;
172 let remainder = n_cells % n_gpus;
173 let mut ranges = Vec::with_capacity(n_gpus);
174 let mut start = 0;
175 for gpu in 0..n_gpus {
176 let extra = if gpu < remainder { 1 } else { 0 };
177 let end = start + base + extra;
178 ranges.push(start..end);
179 start = end;
180 }
181 ranges
182}
183pub fn gpu_neighbor_search_kernel(
188 cl: &GpuCellList,
189 positions: &[[f64; 3]],
190 cutoff: f64,
191) -> Vec<(usize, usize)> {
192 let mut pairs = Vec::new();
193 cl.for_each_pair(positions, cutoff, |i, j, _d2| {
194 let (a, b) = if i < j { (i, j) } else { (j, i) };
195 pairs.push((a, b));
196 });
197 pairs.sort_unstable();
198 pairs.dedup();
199 pairs
200}
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::cell_list::CellList;
205
206 use crate::cell_list::GpuCellList;
207
208 use crate::cell_list::SpatialHash;
209
210 #[test]
211 fn test_prefix_sum_empty() {
212 assert_eq!(parallel_prefix_sum(&[]), Vec::<usize>::new());
213 }
214 #[test]
215 fn test_prefix_sum_basic() {
216 let counts = [1usize, 2, 3, 4];
217 let result = parallel_prefix_sum(&counts);
218 assert_eq!(result, vec![0, 1, 3, 6]);
219 }
220 #[test]
221 fn test_cell_index_clamp() {
222 let list = GpuCellList::new([4, 4, 4], 1.0, [4.0, 4.0, 4.0]);
223 let idx = list.cell_index([4.5, 4.5, 4.5]);
224 assert_eq!(idx, 3 * 4 * 4 + 3 * 4 + 3);
225 }
226 #[test]
227 fn test_total_cells() {
228 let list = GpuCellList::new([3, 4, 5], 1.0, [3.0, 4.0, 5.0]);
229 assert_eq!(list.total_cells(), 60);
230 }
231 #[test]
232 fn test_build_parallel_counts() {
233 let positions: Vec<[f64; 3]> = vec![
234 [0.5, 0.5, 0.5],
235 [1.5, 0.5, 0.5],
236 [0.5, 1.5, 0.5],
237 [1.5, 1.5, 0.5],
238 [0.5, 0.5, 1.5],
239 [1.5, 0.5, 1.5],
240 [0.5, 1.5, 1.5],
241 [1.5, 1.5, 1.5],
242 ];
243 let cl = GpuCellList::build_parallel(&positions);
244 assert_eq!(cl.sorted_indices.len(), 8);
245 for c in 0..cl.total_cells() {
246 assert_eq!(cl.cell_counts[c], 1);
247 }
248 }
249 #[test]
250 fn test_neighbors_in_radius() {
251 let positions: Vec<[f64; 3]> = vec![[0.5, 0.5, 0.5], [0.6, 0.5, 0.5], [5.0, 5.0, 5.0]];
252 let cl = GpuCellList::build_parallel(&positions);
253 let mut neighbours = cl.neighbors_in_radius(&positions, [0.5, 0.5, 0.5], 0.5);
254 neighbours.sort_unstable();
255 assert!(neighbours.contains(&0));
256 assert!(neighbours.contains(&1));
257 assert!(!neighbours.contains(&2));
258 }
259 #[test]
260 fn cell_list_find_neighbors_all_pairs() {
261 let positions: Vec<[f64; 3]> = vec![
262 [1.0, 1.0, 1.0],
263 [1.2, 1.0, 1.0],
264 [1.0, 1.3, 1.0],
265 [9.0, 9.0, 9.0],
266 ];
267 let cl = CellList::build(&positions);
268 let radius = 0.5;
269 let mut neighbours = cl.find_neighbors([1.0, 1.0, 1.0], radius);
270 neighbours.sort_unstable();
271 assert!(neighbours.contains(&0), "should find self: {neighbours:?}");
272 assert!(
273 neighbours.contains(&1),
274 "should find particle 1: {neighbours:?}"
275 );
276 assert!(
277 neighbours.contains(&2),
278 "should find particle 2: {neighbours:?}"
279 );
280 assert!(
281 !neighbours.contains(&3),
282 "particle 3 is far: {neighbours:?}"
283 );
284 }
285 #[test]
286 fn cell_list_new_compiles() {
287 let cl = CellList::new([10.0, 10.0, 10.0], 2.0);
288 assert_eq!(cl.inner.total_cells(), 125);
289 }
290 #[test]
292 fn test_morton_roundtrip() {
293 let test_cases = [
294 (0, 0, 0),
295 (1, 0, 0),
296 (0, 1, 0),
297 (0, 0, 1),
298 (7, 3, 5),
299 (1023, 1023, 1023),
300 (512, 256, 128),
301 ];
302 for (x, y, z) in test_cases {
303 let code = morton_encode(x, y, z);
304 let (dx, dy, dz) = morton_decode(code);
305 assert_eq!(dx, x, "x mismatch for ({x},{y},{z})");
306 assert_eq!(dy, y, "y mismatch for ({x},{y},{z})");
307 assert_eq!(dz, z, "z mismatch for ({x},{y},{z})");
308 }
309 }
310 #[test]
312 fn test_morton_locality() {
313 let c1 = morton_encode(1, 1, 1);
314 let c2 = morton_encode(2, 1, 1);
315 let c_far = morton_encode(100, 100, 100);
316 let d_near = c1.abs_diff(c2);
317 let d_far = c1.abs_diff(c_far);
318 assert!(
319 d_near < d_far,
320 "near distance {d_near} should be less than far {d_far}"
321 );
322 }
323 #[test]
325 fn test_morton_sort_permutation() {
326 let positions = vec![[5.0, 5.0, 5.0], [1.0, 1.0, 1.0], [3.0, 3.0, 3.0]];
327 let (indices, codes) = morton_sort(&positions, [0.0; 3], [10.0, 10.0, 10.0]);
328 assert_eq!(indices.len(), 3);
329 assert_eq!(codes.len(), 3);
330 for i in 0..codes.len() - 1 {
331 assert!(codes[i] <= codes[i + 1], "codes not sorted at {i}");
332 }
333 let mut sorted = indices.clone();
334 sorted.sort();
335 assert_eq!(sorted, vec![0, 1, 2]);
336 }
337 #[test]
339 fn test_spatial_hash_query() {
340 let positions = vec![[0.5, 0.5, 0.5], [0.6, 0.5, 0.5], [5.0, 5.0, 5.0]];
341 let mut hash = SpatialHash::new(64, 1.0);
342 hash.build(&positions);
343 assert_eq!(hash.len(), 3);
344 let mut neighbours = hash.query_radius(&positions, [0.5, 0.5, 0.5], 0.5);
345 neighbours.sort_unstable();
346 neighbours.dedup();
347 assert!(neighbours.contains(&0));
348 assert!(neighbours.contains(&1));
349 assert!(!neighbours.contains(&2));
350 }
351 #[test]
353 fn test_spatial_hash_empty() {
354 let hash = SpatialHash::new(64, 1.0);
355 assert!(hash.is_empty());
356 assert_eq!(hash.len(), 0);
357 }
358 #[test]
360 fn test_spatial_hash_clear() {
361 let mut hash = SpatialHash::new(64, 1.0);
362 hash.insert(0, [0.5, 0.5, 0.5]);
363 assert!(!hash.is_empty());
364 hash.clear();
365 assert!(hash.is_empty());
366 }
367 #[test]
369 fn test_bounding_box() {
370 let positions = vec![[1.0, 2.0, 3.0], [4.0, 0.0, 1.0], [2.0, 5.0, 2.0]];
371 let (min, max) = compute_bounding_box(&positions);
372 assert_eq!(min, [1.0, 0.0, 1.0]);
373 assert_eq!(max, [4.0, 5.0, 3.0]);
374 }
375 #[test]
377 fn test_bounding_box_empty() {
378 let (min, max) = compute_bounding_box(&[]);
379 assert_eq!(min, [0.0; 3]);
380 assert_eq!(max, [0.0; 3]);
381 }
382 #[test]
384 fn test_reorder() {
385 let data = vec![10, 20, 30, 40];
386 let perm = vec![3, 1, 0, 2];
387 let reordered = reorder_by_permutation(&data, &perm);
388 assert_eq!(reordered, vec![40, 20, 10, 30]);
389 }
390 #[test]
392 fn test_max_cell_occupancy() {
393 let positions: Vec<[f64; 3]> = vec![[0.5, 0.5, 0.5], [0.6, 0.5, 0.5], [5.0, 5.0, 5.0]];
394 let cl = GpuCellList::build_parallel(&positions);
395 let max = cl.max_cell_occupancy();
396 assert!(max >= 2, "max occupancy should be at least 2, got {max}");
397 }
398 #[test]
400 fn test_nonempty_cells() {
401 let positions: Vec<[f64; 3]> = vec![[0.5, 0.5, 0.5], [5.0, 5.0, 5.0]];
402 let cl = GpuCellList::build_parallel(&positions);
403 let ne = cl.num_nonempty_cells();
404 assert_eq!(ne, 2, "should have 2 non-empty cells, got {ne}");
405 }
406 #[test]
408 fn test_for_each_pair() {
409 let positions: Vec<[f64; 3]> = vec![[0.5, 0.5, 0.5], [0.6, 0.5, 0.5], [5.0, 5.0, 5.0]];
410 let cl = GpuCellList::build_parallel(&positions);
411 let mut pairs = Vec::new();
412 cl.for_each_pair(&positions, 0.5, |i, j, _d2| {
413 pairs.push((i.min(j), i.max(j)));
414 });
415 pairs.sort();
416 pairs.dedup();
417 assert!(
418 pairs.contains(&(0, 1)),
419 "should find pair (0,1), got {pairs:?}"
420 );
421 assert!(
422 !pairs.iter().any(|&(a, b)| a == 2 || b == 2),
423 "should not find pairs with particle 2"
424 );
425 }
426 #[test]
427 fn test_radix_sort_sorted_output() {
428 let keys = vec![5u32, 1, 9, 3, 7, 2];
429 let (sorted_keys, sorted_indices) = radix_sort_mock(&keys);
430 for i in 0..sorted_keys.len() - 1 {
431 assert!(
432 sorted_keys[i] <= sorted_keys[i + 1],
433 "radix sort not sorted at {i}"
434 );
435 }
436 for &idx in &sorted_indices {
437 assert!(idx < keys.len(), "invalid index {idx}");
438 }
439 }
440 #[test]
441 fn test_radix_sort_permutation_correct() {
442 let keys = vec![30u32, 10, 20];
443 let (sorted_keys, sorted_indices) = radix_sort_mock(&keys);
444 assert_eq!(sorted_keys[0], 10);
445 assert_eq!(sorted_keys[1], 20);
446 assert_eq!(sorted_keys[2], 30);
447 assert_eq!(sorted_indices[0], 1);
448 assert_eq!(sorted_indices[1], 2);
449 assert_eq!(sorted_indices[2], 0);
450 }
451 #[test]
452 fn test_radix_sort_empty() {
453 let keys: Vec<u32> = vec![];
454 let (sk, si) = radix_sort_mock(&keys);
455 assert!(sk.is_empty());
456 assert!(si.is_empty());
457 }
458 #[test]
459 fn test_radix_sort_all_equal() {
460 let keys = vec![7u32; 10];
461 let (sorted_keys, sorted_indices) = radix_sort_mock(&keys);
462 assert_eq!(sorted_keys.len(), 10);
463 assert!(sorted_keys.iter().all(|&k| k == 7));
464 assert_eq!(sorted_indices.len(), 10);
465 }
466 #[test]
467 fn test_gpu_prefix_sum_basic() {
468 let counts = vec![0usize, 1, 3, 0, 2, 5];
469 let result = gpu_prefix_sum(&counts);
470 assert_eq!(result, vec![0, 0, 1, 4, 4, 6]);
471 }
472 #[test]
473 fn test_gpu_prefix_sum_all_zeros() {
474 let counts = vec![0usize; 5];
475 let result = gpu_prefix_sum(&counts);
476 assert_eq!(result, vec![0, 0, 0, 0, 0]);
477 }
478 #[test]
479 fn test_gpu_prefix_sum_single() {
480 let result = gpu_prefix_sum(&[7usize]);
481 assert_eq!(result, vec![0]);
482 }
483 #[test]
484 fn test_parallel_cell_counting() {
485 let positions = vec![[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [2.5, 0.5, 0.5]];
486 let n_cells = [4usize, 4, 4];
487 let counts = parallel_count_particles(&positions, n_cells, 1.0);
488 let total: usize = counts.iter().sum();
489 assert_eq!(total, 3, "total count should equal number of particles");
490 }
491 #[test]
492 fn test_parallel_cell_counting_all_in_one_cell() {
493 let positions: Vec<[f64; 3]> = vec![[0.1, 0.1, 0.1], [0.2, 0.1, 0.1], [0.1, 0.2, 0.1]];
494 let counts = parallel_count_particles(&positions, [4, 4, 4], 1.0);
495 let max = counts.iter().cloned().max().unwrap_or(0);
496 assert!(max >= 3, "all particles in one cell: max_count={max}");
497 }
498 #[test]
499 fn test_multi_gpu_distribution_two_gpus() {
500 let n_cells = 100;
501 let n_gpus = 2;
502 let ranges = distribute_cells_to_gpus(n_cells, n_gpus);
503 assert_eq!(ranges.len(), n_gpus);
504 assert_eq!(ranges[0].start, 0);
505 assert_eq!(ranges[n_gpus - 1].end, n_cells);
506 for i in 0..n_gpus - 1 {
507 assert_eq!(ranges[i].end, ranges[i + 1].start, "gap at gpu {i}");
508 }
509 }
510 #[test]
511 fn test_multi_gpu_distribution_odd_cells() {
512 let ranges = distribute_cells_to_gpus(7, 3);
513 assert_eq!(ranges.len(), 3);
514 let total: usize = ranges.iter().map(|r| r.end - r.start).sum();
515 assert_eq!(total, 7);
516 }
517 #[test]
518 fn test_multi_gpu_distribution_single_gpu() {
519 let ranges = distribute_cells_to_gpus(50, 1);
520 assert_eq!(ranges.len(), 1);
521 assert_eq!(ranges[0].start, 0);
522 assert_eq!(ranges[0].end, 50);
523 }
524 #[test]
525 fn test_neighbor_search_kernel_finds_close_pair() {
526 let positions: Vec<[f64; 3]> = vec![[1.0, 1.0, 1.0], [1.1, 1.0, 1.0], [5.0, 5.0, 5.0]];
527 let cl = GpuCellList::build_parallel(&positions);
528 let pairs = gpu_neighbor_search_kernel(&cl, &positions, 0.5);
529 assert!(
530 pairs.contains(&(0, 1)) || pairs.contains(&(1, 0)),
531 "should find pair (0,1), got {pairs:?}"
532 );
533 assert!(
534 !pairs.iter().any(|&(a, b)| a == 2 || b == 2),
535 "particle 2 should not appear in pairs"
536 );
537 }
538 #[test]
539 fn test_neighbor_search_kernel_no_pairs() {
540 let positions: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [20.0, 0.0, 0.0]];
541 let cl = GpuCellList::build_parallel(&positions);
542 let pairs = gpu_neighbor_search_kernel(&cl, &positions, 0.5);
543 assert!(pairs.is_empty(), "well-separated particles → no pairs");
544 }
545 #[test]
546 fn test_spatial_hash_rebuild() {
547 let mut hash = SpatialHash::new(128, 1.0);
548 let positions1 = vec![[0.5, 0.5, 0.5], [1.5, 0.5, 0.5]];
549 hash.build(&positions1);
550 assert_eq!(hash.len(), 2);
551 let positions2 = vec![[0.1, 0.1, 0.1]];
552 hash.build(&positions2);
553 assert_eq!(hash.len(), 1, "rebuild should replace old data");
554 }
555 #[test]
556 fn test_spatial_hash_large_number_of_particles() {
557 let positions: Vec<[f64; 3]> = (0..200).map(|i| [i as f64 * 0.1, 0.0, 0.0]).collect();
558 let mut hash = SpatialHash::new(256, 1.0);
559 hash.build(&positions);
560 assert_eq!(hash.len(), 200);
561 }
562}
563pub fn parallel_morton_sort(
570 positions: &[[f64; 3]],
571 box_min: [f64; 3],
572 box_max: [f64; 3],
573) -> (Vec<usize>, Vec<u32>) {
574 let range = [
575 (box_max[0] - box_min[0]).max(1e-10),
576 (box_max[1] - box_min[1]).max(1e-10),
577 (box_max[2] - box_min[2]).max(1e-10),
578 ];
579 let mut code_index_pairs: Vec<(u32, usize)> = positions
580 .par_iter()
581 .enumerate()
582 .map(|(i, p)| {
583 let xi = (((p[0] - box_min[0]) / range[0]) * 1023.0) as u32;
584 let yi = (((p[1] - box_min[1]) / range[1]) * 1023.0) as u32;
585 let zi = (((p[2] - box_min[2]) / range[2]) * 1023.0) as u32;
586 let x = xi.min(1023);
587 let y = yi.min(1023);
588 let z = zi.min(1023);
589 (morton_encode(x, y, z), i)
590 })
591 .collect();
592 code_index_pairs.sort_by_key(|&(code, _)| code);
593 let sorted_indices: Vec<usize> = code_index_pairs.iter().map(|&(_, i)| i).collect();
594 let sorted_codes: Vec<u32> = code_index_pairs.iter().map(|&(c, _)| c).collect();
595 (sorted_indices, sorted_codes)
596}
597pub fn position_to_morton(pos: [f64; 3], box_min: [f64; 3], box_max: [f64; 3]) -> u32 {
601 let range = [
602 (box_max[0] - box_min[0]).max(1e-10),
603 (box_max[1] - box_min[1]).max(1e-10),
604 (box_max[2] - box_min[2]).max(1e-10),
605 ];
606 let x = (((pos[0] - box_min[0]) / range[0]) * 1023.0) as u32;
607 let y = (((pos[1] - box_min[1]) / range[1]) * 1023.0) as u32;
608 let z = (((pos[2] - box_min[2]) / range[2]) * 1023.0) as u32;
609 morton_encode(x.min(1023), y.min(1023), z.min(1023))
610}
611pub fn insert_particles(cl: &mut GpuCellList, new_positions: &[[f64; 3]]) -> usize {
620 let old_n = cl.sorted_indices.len();
621 let mut inserted = 0usize;
622 for (i, &pos) in new_positions.iter().enumerate() {
623 let cell = cl.cell_index(pos);
624 cl.sorted_indices.push(old_n + i);
625 cl.cell_counts[cell] += 1;
626 inserted += 1;
627 }
628 let new_starts = parallel_prefix_sum(
629 &cl.cell_counts
630 .iter()
631 .map(|&c| c as usize)
632 .collect::<Vec<_>>(),
633 );
634 cl.cell_starts = new_starts.iter().map(|&s| s as i32).collect();
635 inserted
636}
637pub fn query_neighbors(
643 cl: &GpuCellList,
644 positions: &[[f64; 3]],
645 query_pos: [f64; 3],
646 radius: f64,
647) -> Vec<usize> {
648 cl.neighbors_in_radius(positions, query_pos, radius)
649}
650#[cfg(test)]
651mod extended_cell_tests {
652 use crate::cell_list::CellList;
653 use crate::cell_list::GhostCellManager;
654 use crate::cell_list::GpuCellList;
655 use crate::cell_list::GridResizer;
656 use crate::cell_list::OccupancyStats;
657
658 use crate::cell_list::insert_particles;
659 use crate::cell_list::parallel_morton_sort;
660 use crate::cell_list::position_to_morton;
661 use crate::cell_list::query_neighbors;
662 #[test]
663 fn test_occupancy_stats_uniform() {
664 let positions: Vec<[f64; 3]> = vec![
665 [0.5, 0.5, 0.5],
666 [1.5, 0.5, 0.5],
667 [0.5, 1.5, 0.5],
668 [1.5, 1.5, 0.5],
669 [0.5, 0.5, 1.5],
670 [1.5, 0.5, 1.5],
671 [0.5, 1.5, 1.5],
672 [1.5, 1.5, 1.5],
673 ];
674 let cl = GpuCellList::build_parallel(&positions);
675 let stats = OccupancyStats::compute(&cl);
676 assert_eq!(stats.total_particles, 8);
677 assert_eq!(stats.max_occupancy, 1);
678 assert!(stats.is_perfectly_spread());
679 }
680 #[test]
681 fn test_occupancy_stats_clustered() {
682 let positions: Vec<[f64; 3]> = vec![
683 [0.1, 0.1, 0.1],
684 [0.2, 0.1, 0.1],
685 [0.1, 0.2, 0.1],
686 [10.0, 10.0, 10.0],
687 ];
688 let cl = GpuCellList::build_parallel(&positions);
689 let stats = OccupancyStats::compute(&cl);
690 assert_eq!(stats.total_particles, 4);
691 assert!(
692 stats.max_occupancy >= 2,
693 "clustered particles should share a cell"
694 );
695 assert_eq!(stats.nonempty_cells, 2);
696 }
697 #[test]
698 fn test_occupancy_stats_load_imbalance_uniform() {
699 let positions: Vec<[f64; 3]> = vec![[0.5, 0.5, 0.5], [1.5, 0.5, 0.5]];
700 let cl = GpuCellList::build_parallel(&positions);
701 let stats = OccupancyStats::compute(&cl);
702 assert!(
703 (stats.load_imbalance - 1.0).abs() < 1e-10,
704 "load_imbalance = {}",
705 stats.load_imbalance
706 );
707 }
708 #[test]
709 fn test_occupancy_stats_completely_unbalanced() {
710 let positions: Vec<[f64; 3]> = vec![[0.1, 0.1, 0.1], [0.2, 0.2, 0.2], [0.3, 0.1, 0.1]];
711 let cl = GpuCellList::build_parallel(&positions);
712 let stats = OccupancyStats::compute(&cl);
713 assert!(stats.is_completely_unbalanced() || stats.max_occupancy >= 2);
714 }
715 #[test]
716 fn test_grid_resizer_initial_build() {
717 let mut resizer = GridResizer::new(1.0, 0.5);
718 let positions = vec![[1.0, 1.0, 1.0], [3.0, 3.0, 3.0]];
719 resizer.update(&positions);
720 assert!(resizer.get().is_some(), "cell list should be built");
721 }
722 #[test]
723 fn test_grid_resizer_no_resize_needed() {
724 let mut resizer = GridResizer::new(1.0, 1.0);
725 let positions = vec![[2.0, 2.0, 2.0]];
726 resizer.update(&positions);
727 let needs = resizer.needs_resize(&positions);
728 assert!(!needs, "same positions should not need resize");
729 }
730 #[test]
731 fn test_grid_resizer_escaping_particle() {
732 let mut resizer = GridResizer::new(1.0, 0.5);
733 let positions = vec![[1.0, 1.0, 1.0]];
734 resizer.rebuild(&positions);
735 let new_positions = vec![[100.0, 100.0, 100.0]];
736 assert!(
737 resizer.needs_resize(&new_positions),
738 "escaped particle should trigger resize"
739 );
740 }
741 #[test]
742 fn test_grid_resizer_empty_positions() {
743 let mut resizer = GridResizer::new(1.0, 0.5);
744 resizer.rebuild(&[]);
745 assert!(
746 resizer.get().is_some(),
747 "empty rebuild should produce valid list"
748 );
749 }
750 #[test]
751 fn test_ghost_manager_no_ghosts_interior() {
752 let mut mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.0);
753 let positions = vec![[5.0, 5.0, 5.0], [6.0, 6.0, 6.0]];
754 mgr.build_ghosts(&positions);
755 assert_eq!(mgr.num_ghosts(), 0, "interior particles need no ghosts");
756 }
757 #[test]
758 fn test_ghost_manager_near_one_face() {
759 let mut mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.5);
760 let positions = vec![[0.5, 5.0, 5.0]];
761 mgr.build_ghosts(&positions);
762 assert_eq!(mgr.num_ghosts(), 1, "should create 1 ghost on +x side");
763 assert!(
764 (mgr.ghost_positions[0][0] - 10.5).abs() < 1e-10,
765 "ghost x = {}",
766 mgr.ghost_positions[0][0]
767 );
768 }
769 #[test]
770 fn test_ghost_manager_near_two_faces() {
771 let mut mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.5);
772 let positions = vec![[0.5, 0.5, 5.0]];
773 mgr.build_ghosts(&positions);
774 assert_eq!(mgr.num_ghosts(), 2, "particle near two faces → 2 ghosts");
775 }
776 #[test]
777 fn test_ghost_manager_near_corner() {
778 let mut mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.5);
779 let positions = vec![[0.5, 0.5, 0.5]];
780 mgr.build_ghosts(&positions);
781 assert_eq!(mgr.num_ghosts(), 3, "corner particle → 3 primary ghosts");
782 }
783 #[test]
784 fn test_ghost_manager_map_to_real() {
785 let mut mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.0);
786 let positions = vec![[0.5, 5.0, 5.0], [9.5, 5.0, 5.0]];
787 mgr.build_ghosts(&positions);
788 for &ri in &mgr.ghost_to_real {
789 assert!(ri < positions.len(), "real index {ri} out of range");
790 }
791 }
792 #[test]
793 fn test_minimum_image_convention() {
794 let mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.0);
795 let d = mgr.minimum_image([9.0, 0.0, 0.0]);
796 assert!((d[0] - (-1.0)).abs() < 1e-10, "min image x = {}", d[0]);
797 assert!(d[1].abs() < 1e-12);
798 assert!(d[2].abs() < 1e-12);
799 }
800 #[test]
801 fn test_wrap_position_basic() {
802 let mgr = GhostCellManager::new([10.0, 10.0, 10.0], 1.0);
803 let p = mgr.wrap_position([11.5, -0.5, 10.0]);
804 assert!((p[0] - 1.5).abs() < 1e-10, "wrapped x = {}", p[0]);
805 assert!((p[1] - 9.5).abs() < 1e-10, "wrapped y = {}", p[1]);
806 assert!(p[2].abs() < 1e-10, "wrapped z = {}", p[2]);
807 }
808 #[test]
809 fn test_wrap_all_in_place() {
810 let mgr = GhostCellManager::new([5.0, 5.0, 5.0], 0.5);
811 let mut positions = vec![[6.0, 7.0, 0.0], [-1.0, 2.5, 11.0]];
812 mgr.wrap_all(&mut positions);
813 for p in &positions {
814 for &coord in p.iter() {
815 assert!(
816 (0.0..5.0).contains(&coord),
817 "wrapped coord out of range: {}",
818 coord
819 );
820 }
821 }
822 }
823 #[test]
824 fn test_parallel_morton_sort_sorted_codes() {
825 let positions = vec![
826 [3.0, 3.0, 3.0],
827 [1.0, 1.0, 1.0],
828 [7.0, 7.0, 7.0],
829 [5.0, 5.0, 5.0],
830 ];
831 let (_idx, codes) = parallel_morton_sort(&positions, [0.0; 3], [10.0; 3]);
832 for i in 0..codes.len() - 1 {
833 assert!(
834 codes[i] <= codes[i + 1],
835 "parallel morton sort codes not sorted at {i}"
836 );
837 }
838 }
839 #[test]
840 fn test_parallel_morton_sort_valid_permutation() {
841 let positions: Vec<[f64; 3]> = (0..10).map(|i| [i as f64, 0.0, 0.0]).collect();
842 let (idx, codes) = parallel_morton_sort(&positions, [0.0; 3], [10.0, 1.0, 1.0]);
843 assert_eq!(idx.len(), 10);
844 assert_eq!(codes.len(), 10);
845 let mut sorted_idx = idx.clone();
846 sorted_idx.sort_unstable();
847 assert_eq!(sorted_idx, (0..10).collect::<Vec<_>>());
848 }
849 #[test]
850 fn test_position_to_morton_corner() {
851 let code = position_to_morton([0.0, 0.0, 0.0], [0.0; 3], [1.0; 3]);
852 assert_eq!(code, 0, "corner should give Morton code 0");
853 }
854 #[test]
855 fn test_position_to_morton_different_positions() {
856 let p1 = position_to_morton([1.0, 0.0, 0.0], [0.0; 3], [10.0; 3]);
857 let p2 = position_to_morton([0.0, 1.0, 0.0], [0.0; 3], [10.0; 3]);
858 let p3 = position_to_morton([5.0, 5.0, 5.0], [0.0; 3], [10.0; 3]);
859 assert_ne!(p1, p3);
860 assert_ne!(p2, p3);
861 }
862 #[test]
863 fn test_insert_particles_increases_count() {
864 let mut cl = GpuCellList::build_parallel(&[[1.0, 1.0, 1.0]]);
865 let original_len = cl.sorted_indices.len();
866 let new_particles = vec![[2.0, 2.0, 2.0], [3.0, 3.0, 3.0]];
867 let inserted = insert_particles(&mut cl, &new_particles);
868 assert_eq!(inserted, 2);
869 assert_eq!(cl.sorted_indices.len(), original_len + 2);
870 }
871 #[test]
872 fn test_insert_particles_empty_grid() {
873 let mut cl = GpuCellList::new([4, 4, 4], 1.0, [4.0, 4.0, 4.0]);
874 let positions = vec![[0.5, 0.5, 0.5], [1.5, 0.5, 0.5]];
875 let inserted = insert_particles(&mut cl, &positions);
876 assert_eq!(inserted, 2);
877 }
878 #[test]
879 fn test_query_neighbors_finds_close_particle() {
880 let positions = vec![[1.0, 1.0, 1.0], [1.1, 1.0, 1.0], [9.0, 9.0, 9.0]];
881 let cl = GpuCellList::build_parallel(&positions);
882 let mut neighbours = query_neighbors(&cl, &positions, [1.0, 1.0, 1.0], 0.5);
883 neighbours.sort_unstable();
884 assert!(neighbours.contains(&0), "should find self");
885 assert!(neighbours.contains(&1), "should find nearby particle");
886 assert!(!neighbours.contains(&2), "should not find far particle");
887 }
888 #[test]
889 fn test_query_neighbors_empty_result() {
890 let positions = vec![[0.0, 0.0, 0.0], [100.0, 100.0, 100.0]];
891 let cl = GpuCellList::build_parallel(&positions);
892 let neighbours = query_neighbors(&cl, &positions, [50.0, 50.0, 50.0], 0.1);
893 assert!(neighbours.is_empty(), "no particles near middle of box");
894 }
895 #[test]
896 fn test_verlet_list_close_pair_found() {
897 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [10.0, 10.0, 10.0]];
898 let cl = CellList::build(&positions);
899 let pairs = cl.build_neighbor_list_verlet(1.0, 0.2);
900 let has_01 = pairs.contains(&(0, 1));
901 assert!(has_01, "pair (0,1) must be in Verlet list");
902 }
903 #[test]
904 fn test_verlet_list_far_pair_excluded() {
905 let positions = vec![[0.0, 0.0, 0.0], [20.0, 20.0, 20.0]];
906 let cl = CellList::build(&positions);
907 let pairs = cl.build_neighbor_list_verlet(1.0, 0.2);
908 assert!(pairs.is_empty(), "far pair must not appear in Verlet list");
909 }
910 #[test]
911 fn test_verlet_list_no_self_pairs() {
912 let positions = vec![[1.0, 1.0, 1.0], [1.1, 1.0, 1.0], [1.2, 1.0, 1.0]];
913 let cl = CellList::build(&positions);
914 let pairs = cl.build_neighbor_list_verlet(1.0, 0.5);
915 for &(i, j) in &pairs {
916 assert_ne!(i, j, "self-pair found");
917 }
918 }
919 #[test]
920 fn test_verlet_list_pairs_ordered() {
921 let positions: Vec<[f64; 3]> = (0..5).map(|i| [i as f64 * 0.3, 0.0, 0.0]).collect();
922 let cl = CellList::build(&positions);
923 let pairs = cl.build_neighbor_list_verlet(1.0, 0.1);
924 for &(i, j) in &pairs {
925 assert!(i < j, "Verlet pair must have i < j");
926 }
927 }
928 #[test]
929 fn test_update_incremental_no_move() {
930 let positions = vec![[1.0, 1.0, 1.0], [3.0, 3.0, 3.0]];
931 let mut cl = CellList::build(&positions);
932 let relocated = cl.update_incremental(&positions, &positions, 0.1);
933 assert_eq!(relocated, 0, "no particle moved");
934 }
935 #[test]
936 fn test_update_incremental_large_move_counted() {
937 let old = vec![[1.0, 1.0, 1.0], [3.0, 3.0, 3.0]];
938 let new_pos = vec![[1.0, 1.0, 1.0], [6.0, 6.0, 6.0]];
939 let mut cl = CellList::build(&old);
940 let relocated = cl.update_incremental(&new_pos, &old, 0.5);
941 assert!(relocated >= 1, "at least one particle relocated");
942 }
943 #[test]
944 fn test_update_incremental_threshold_respected() {
945 let old = vec![[0.0, 0.0, 0.0], [5.0, 5.0, 5.0]];
946 let new_pos = vec![[0.05, 0.0, 0.0], [8.0, 8.0, 8.0]];
947 let mut cl = CellList::build(&old);
948 let relocated = cl.update_incremental(&new_pos, &old, 1.0);
949 assert_eq!(relocated, 1);
950 }
951 #[test]
952 fn test_pair_density_single_bin() {
953 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
954 let cl = CellList::build(&positions);
955 let hist = cl.compute_pair_density(2.0, 1.0);
956 assert!(hist[0] >= 1, "pair must appear in bin 0");
957 }
958 #[test]
959 fn test_pair_density_no_pairs_beyond_max_r() {
960 let positions = vec![[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
961 let cl = CellList::build(&positions);
962 let hist = cl.compute_pair_density(2.0, 0.5);
963 let total: usize = hist.iter().sum();
964 assert_eq!(total, 0, "pair beyond max_r should not be counted");
965 }
966 #[test]
967 fn test_pair_density_histogram_length() {
968 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
969 let cl = CellList::build(&positions);
970 let hist = cl.compute_pair_density(5.0, 1.0);
971 assert_eq!(hist.len(), 5, "histogram length = ceil(max_r/dr)");
972 }
973}