From f3aa847ba2627fe2010dca57002fc9f90faee45d Mon Sep 17 00:00:00 2001 From: fncnt Date: Wed, 11 Feb 2026 17:58:46 +0100 Subject: [PATCH 01/11] WIP: Re-work of the Particle API --- benches/cellgrid.rs | 26 +++++-- benches/iters.rs | 14 +++- benches/lj.rs | 22 ++++-- examples/cachemisses.rs | 18 ++++- examples/minimal.rs | 14 +++- src/cellgrid.rs | 4 +- src/lib.rs | 121 +++++++++++++++++++++++++++++-- surface-sampling/examples/cli.rs | 7 +- surface-sampling/src/sdf.rs | 11 ++- 9 files changed, 202 insertions(+), 35 deletions(-) diff --git a/benches/cellgrid.rs b/benches/cellgrid.rs index d05e1f0..ba9a197 100644 --- a/benches/cellgrid.rs +++ b/benches/cellgrid.rs @@ -5,9 +5,9 @@ use criterion::{ use nalgebra::{Point, Point3, Vector3, distance_squared}; use rand::distributions::Standard; use rand::prelude::*; -use zelll::CellGrid; #[cfg(feature = "rayon")] use zelll::rayon::ParallelIterator; +use zelll::{CellGrid, WrappedParticle}; type F32or64 = f64; @@ -53,12 +53,28 @@ pub fn bench_cellgrid(c: &mut Criterion) { let pointcloud = generate_points_random(size, [a, b, c], [0.0, 0.0, 0.0], None); // pointcloud.sort_unstable_by(|p, q| p.z.partial_cmp(&q.z).unwrap()); - let cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); group.bench_with_input( BenchmarkId::new("::new()", size), &pointcloud, - |b, pointcloud| b.iter(|| CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff)), + |b, pointcloud| { + b.iter(|| { + CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ) + }) + }, ); group.bench_with_input( @@ -69,7 +85,7 @@ pub fn bench_cellgrid(c: &mut Criterion) { b.iter(|| { cg.particle_pairs() .filter(|&((_i, p), (_j, q))| { - distance_squared(&p.into(), &q.into()) <= cutoff_squared + distance_squared(&(*p).into(), &(*q).into()) <= cutoff_squared }) .for_each(|_| {}); }) @@ -84,7 +100,7 @@ pub fn bench_cellgrid(c: &mut Criterion) { let cutoff_squared = cutoff.powi(2); b.iter(|| { cg.par_particle_pairs().for_each(|((_i, p), (_j, q))| { - if distance_squared(&p.into(), &q.into()) <= cutoff_squared { + if distance_squared(&(*p).into(), &(*q).into()) <= cutoff_squared { } else { } }); diff --git a/benches/iters.rs b/benches/iters.rs index 6a943e6..0157cf7 100644 --- a/benches/iters.rs +++ b/benches/iters.rs @@ -8,9 +8,9 @@ use rand::distributions::Standard; use rand::prelude::*; #[cfg(feature = "rayon")] use rayon::ThreadPoolBuilder; -use zelll::CellGrid; #[cfg(feature = "rayon")] use zelll::rayon::ParallelIterator; +use zelll::{CellGrid, WrappedParticle}; type F32or64 = f64; @@ -56,7 +56,13 @@ pub fn bench_iters(c: &mut Criterion) { let pointcloud = generate_points_random(size, [a, b, c], [0.0, 0.0, 0.0], None); // pointcloud.sort_unstable_by(|p, q| p.z.partial_cmp(&q.z).unwrap()); - let cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); #[cfg(not(feature = "rayon"))] group.bench_with_input(BenchmarkId::new("sequential", size), &cg, |b, cg| { @@ -64,7 +70,7 @@ pub fn bench_iters(c: &mut Criterion) { b.iter(|| { cg.particle_pairs() .filter(|&((_i, p), (_j, q))| { - distance_squared(&p.into(), &q.into()) <= cutoff_squared + distance_squared(&(*p).into(), &(*q).into()) <= cutoff_squared }) .for_each(|_| {}); }) @@ -85,7 +91,7 @@ pub fn bench_iters(c: &mut Criterion) { b.iter(|| { pool.install(|| { cg.par_particle_pairs().for_each(|((_i, p), (_j, q))| { - if distance_squared(&p.into(), &q.into()) <= cutoff_squared { + if distance_squared(&(*p).into(), &(*q).into()) <= cutoff_squared { } else { } }) diff --git a/benches/lj.rs b/benches/lj.rs index 4232b37..1278704 100644 --- a/benches/lj.rs +++ b/benches/lj.rs @@ -6,7 +6,7 @@ use criterion::{ use nalgebra::{Point, Point3, Vector3, distance_squared}; use rand::distributions::Standard; use rand::prelude::*; -use zelll::CellGrid; +use zelll::{CellGrid, WrappedParticle}; type F32or64 = f64; @@ -70,11 +70,17 @@ pub fn bench_lj(c: &mut Criterion) { // so Massif doesn't keep it in its snapshots the whole time { let cutoff_squared = cutoff.powi(2); - let cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); let potential_energy: F32or64 = cg .particle_pairs() .filter_map(|((_i, p), (_j, q))| { - let dsq = distance_squared(&p.into(), &q.into()); + let dsq = distance_squared(&(*p).into(), &(*q).into()); if dsq < cutoff_squared { Some(dsq) } else { @@ -92,11 +98,17 @@ pub fn bench_lj(c: &mut Criterion) { |b, pointcloud| { b.iter(|| { let cutoff_squared = cutoff.powi(2); - let cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); let _potential_energy: F32or64 = cg .particle_pairs() .filter_map(|((_i, p), (_j, q))| { - let dsq = distance_squared(&p.into(), &q.into()); + let dsq = distance_squared(&(*p).into(), &(*q).into()); if dsq < cutoff_squared { Some(dsq) } else { diff --git a/examples/cachemisses.rs b/examples/cachemisses.rs index c43500e..9ab26df 100644 --- a/examples/cachemisses.rs +++ b/examples/cachemisses.rs @@ -8,7 +8,7 @@ use crabgrind::callgrind as valgrind; use nalgebra::{Point, Point3, Vector3}; use rand::distributions::Standard; use rand::prelude::*; -use zelll::CellGrid; +use zelll::{CellGrid, WrappedParticle}; type PointCloud = Vec>; /// Generate a uniformly random 3D point cloud of size `n` in a cuboid of edge lengths `vol` centered around `origin`. @@ -64,13 +64,25 @@ fn main() { valgrind::start_instrumentation(); for _ in 0..repeat { - let _cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let _cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); } valgrind::stop_instrumentation(); } else { valgrind::start_instrumentation(); for _ in 0..repeat { - let _cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let _cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); } valgrind::stop_instrumentation(); } diff --git a/examples/minimal.rs b/examples/minimal.rs index d988845..b1f11c0 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -6,7 +6,7 @@ use rand::prelude::*; #[cfg(feature = "rayon")] use rayon::prelude::ParallelIterator; use std::hint::black_box; -use zelll::CellGrid; +use zelll::{CellGrid, WrappedParticle}; type PointCloud = Vec>; /// Generate a uniformly random 3D point cloud of size `n` in a cuboid of edge lengths `vol` centered around `origin`. @@ -38,7 +38,13 @@ fn main() { // linear probing in HashMap would help maybe? //pointcloud.sort_unstable_by(|p, q| p.z.partial_cmp(&q.z).unwrap()); - let cg = CellGrid::new(pointcloud.iter().map(|p| p.coords), cutoff); + let cg = CellGrid::new( + pointcloud + .iter() + .map(|p| p.coords) + .map(WrappedParticle::from), + cutoff, + ); println!("{:?}", cg.info().shape()); let _cutoff_squared = cutoff.powi(2); @@ -46,7 +52,9 @@ fn main() { #[cfg(not(feature = "rayon"))] // let count = cg.point_pairs().count(); cg.particle_pairs() - .filter(|&((_i, p), (_j, q))| distance_squared(&p.into(), &q.into()) <= _cutoff_squared) + .filter(|&((_i, p), (_j, q))| { + distance_squared(&(*p).into(), &(*q).into()) <= _cutoff_squared + }) .for_each(|_| black_box(())); // cg.rebuild_mut(pointcloud.iter().rev().map(|p| p.coords), None); diff --git a/src/cellgrid.rs b/src/cellgrid.rs index d209d92..f0ea445 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -100,11 +100,11 @@ pub use util::{Aabb, GridInfo}; /// ``` /// Any type implementing [`Particle`] can be used: /// ``` -/// # use zelll::CellGrid; +/// # use zelll::{CellGrid, WrappedParticle}; /// use nalgebra::SVector; /// /// let data: Vec> = vec![[0.0, 0.0].into(), [1.0,2.0].into(), [0.0, 0.1].into()]; -/// let mut cg = CellGrid::new(data.iter().copied(), 1.0); +/// let mut cg = CellGrid::new(data.iter().copied().map(WrappedParticle::from), 1.0); /// ``` #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/lib.rs b/src/lib.rs index 79c8d3b..0859e1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,13 +124,85 @@ pub trait Particle: Copy { // TODO: Might consider restricting this impl. // TODO: While we can be this generic, this might help articulating our intentions better: // TODO: impl Particle<[T; N]> for P where P: Into<[T; N]> + Copy { -impl Particle for P +// impl Particle<[T; N]> for P +// where +// P: Into<[T; N]> + Copy, +// { +// #[inline] +// fn coords(&self) -> [T; N] { +//

>::into(*self) +// } +// } + +// #[derive(Copy, Clone)] +// pub struct LabeledParticle(L, T); + +// impl> Particle for LabeledParticle { +// fn coords(&self) -> T { +// self.1.coords() +// } +// } + +// TODO: maybe name this Particle, and the trait should be ParticleLike? +// TODO: could have additional trait alias IntoParticle: Into<[T; N]> + Copy where T: Copy? +// TODO: then blanket impl ParticleLike on IntoParticle? is that possible? +// TODO: alternative: IntoParticle could be somewhat object-safe in the sense that it has +// TODO: a trait method returning (formerly Wrapped)Particle

+// TODO: maybe it would be object-safe it has an associated type instead of parameter P +// TODO: if I make (Wrapped)Particle a tuple struct again, we wouldn't need the from impl anymore +// TODO: which would turn .map(WrappedParticle::from) into .map(WrappedParticle) +// TODO: ( or rather .map(Particle) after renaming) +#[derive(Copy, Clone, Debug, Default)] +pub struct WrappedParticle

{ + inner: P, +} + +impl Particle<[T; N]> for WrappedParticle

+where + // P: Particle, + P: Into<[T; N]> + Copy, +{ + fn coords(&self) -> [T; N] { + // self.inner.coords() +

>::into(self.inner) + } +} + +use std::ops::Deref; + +impl

Deref for WrappedParticle

{ + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl

From

for WrappedParticle

{ + fn from(value: P) -> Self { + Self { inner: value } + } +} + +impl Particle<[T; N]> for (usize, P) where - P: Into + Copy, + /* P: Into<[T; N]> + Copy, */ + P: Particle<[T; N]>, { #[inline] - fn coords(&self) -> T /* [T; N] */ { -

>::into(*self) + fn coords(&self) -> [T; N] /* [T; N] */ { + /*

>::into(self.1) */ + self.1.coords() + } +} + +impl Particle<[T; N]> for [T; N] +where + T: Copy, +{ + #[inline] + fn coords(&self) -> [T; N] { + *self } } @@ -198,6 +270,38 @@ mod tests { } } + #[test] + fn test_wrapped_particle() { + // let x: WrappedParticle<_> = [0.0f64; 3].into(); + let x = WrappedParticle::from([0.0f64; 3]); + x.coords(); // calls impl Particle for WrappedParticle

+ (*x).coords(); // calls implParticle for P where P: Particle + + let points = std::iter::repeat_n(x, 10); + + let _ = points.clone().enumerate().map(|ix| ix.coords()); + let _ = points.enumerate().map(|(_i, x)| x.coords()); + + let points = std::iter::repeat_n([0.0f64; 3], 10); + + let _ = points.clone().enumerate().map(|ix| ix.coords()); + let _ = points.enumerate().map(|(_i, x)| x.coords()); + + let points = std::iter::repeat_n(SVector::from([0.0f64; 3]), 10); + + let _ = points + .clone() + .map(WrappedParticle::from) + .enumerate() + .map(|ix| -> [f64; 3] { ix.coords() }); + + let _ = points + .clone() + .map(WrappedParticle::from) + .enumerate() + .map(|(_i, x)| -> [f64; 3] { x.coords() }); + } + #[test] fn test_impl_particle() { let points = vec![[0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3]]; @@ -205,7 +309,11 @@ mod tests { let _ps = PStorage::<_, SparseGrid>::new(points.iter().copied()); let _ps: PStorage<_> = PStorage::new(points.clone().into_iter()); - let points: Vec<_> = points.into_iter().map(|p| SVector::from(p)).collect(); + let points: Vec<_> = points + .into_iter() + .map(SVector::from) + .map(WrappedParticle::from) + .collect(); let ps = PStorage::new_sparse(points.iter().copied()); let _: Vec<[_; 3]> = ps.convert(); @@ -225,8 +333,7 @@ mod tests { let points = vec![[0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3]]; - let ps: PStorage = - PStorage::new(points.iter().map(|p| ParticleRef(p)).clone()); + let ps: PStorage = PStorage::new(points.iter().map(ParticleRef).clone()); let _: Vec<[_; 3]> = ps.convert(); } } diff --git a/surface-sampling/examples/cli.rs b/surface-sampling/examples/cli.rs index 2bc1c8f..b493112 100644 --- a/surface-sampling/examples/cli.rs +++ b/surface-sampling/examples/cli.rs @@ -6,7 +6,7 @@ use psssh::io::PointCloud; use psssh::sdf::SmoothDistanceField; use std::path::PathBuf; use std::time::Instant; -use zelll::Particle; +use zelll::{Particle, WrappedParticle}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -106,7 +106,10 @@ fn main() { // .map_or([0.0; 3], |atom| atom.coords()); // TODO: alternatively, let's just use the first atom, assuming it's at one of the ends // TODO: we're discarding the first `b` samples anyway - let init = data.points.first().map_or([0.0; 3], |atom| atom.coords()); + let init = data + .points + .first() + .map_or([0.0; 3], |atom| WrappedParticle::from(atom).coords()); sampler .set_position(init.as_slice()) diff --git a/surface-sampling/src/sdf.rs b/surface-sampling/src/sdf.rs index e72b95e..d6cecca 100644 --- a/surface-sampling/src/sdf.rs +++ b/surface-sampling/src/sdf.rs @@ -7,11 +7,11 @@ pub use numdual::*; use crate::Angstrom; use crate::atom::Atom; use crate::io::PointCloud; -use zelll::CellGrid; +use zelll::{CellGrid, WrappedParticle}; #[derive(Clone, Debug)] pub struct SmoothDistanceField { - inner: CellGrid, + inner: CellGrid, 3, Angstrom>, pub(crate) surface_radius: Angstrom, pub(crate) k_force: Angstrom, // this wouldn't survive dimensional analysis though } @@ -19,7 +19,10 @@ pub struct SmoothDistanceField { impl SmoothDistanceField { pub fn new(protein: &PointCloud, cutoff: Angstrom) -> Self { Self { - inner: CellGrid::new(protein.points.iter().copied(), cutoff), + inner: CellGrid::new( + protein.points.iter().copied().map(WrappedParticle::from), + cutoff, + ), surface_radius: 1.05, k_force: 10.0, } @@ -39,7 +42,7 @@ impl SmoothDistanceField { Self { k_force, ..self } } - pub fn grid(&self) -> &CellGrid { + pub fn grid(&self) -> &CellGrid, 3, Angstrom> { &self.inner } } From f9aa11043fbe75775851aa997358050a3660f7f0 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 09:59:56 +0100 Subject: [PATCH 02/11] API: Rename trait Particle to ParticleLike --- src/cellgrid.rs | 12 ++++---- src/cellgrid/flatindex.rs | 6 ++-- src/cellgrid/iters.rs | 8 ++--- src/cellgrid/util.rs | 6 ++-- src/lib.rs | 45 ++++++++--------------------- surface-sampling/src/sdf/numdual.rs | 2 +- 6 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/cellgrid.rs b/src/cellgrid.rs index f0ea445..a1b95ea 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -11,7 +11,7 @@ mod storage; #[allow(dead_code)] pub mod util; -use crate::Particle; +use crate::ParticleLike; #[cfg(feature = "rayon")] use crate::rayon::ParallelIterator; use flatindex::FlatIndex; @@ -129,7 +129,7 @@ where // TODO: however for sequential data (biomolecules) this may destroy implicit order (so that would have to be modelled implicitley) // TODO: also, with reordered data, leapfrog integration buffers have to be shuffled accordingly // TODO: which is not that nice -impl, const N: usize, T> CellGrid +impl, const N: usize, T> CellGrid where T: Float + NumAssignOps @@ -321,7 +321,7 @@ where } } -impl, const N: usize, T> CellGrid +impl, const N: usize, T> CellGrid where T: Float + ConstOne + AsPrimitive + std::fmt::Debug + NumAssignOps + Send + Sync, P: Send + Sync, @@ -388,7 +388,7 @@ where /// This restriction might be lifted in the future. /// /// - pub fn query>(&self, particle: Q) -> Option> { + pub fn query>(&self, particle: Q) -> Option> { self.info() .try_cell_index(particle.coords()) .map(|index| self.info().flatten_index(index)) @@ -419,7 +419,7 @@ where /// }); /// ``` #[must_use = "iterators are lazy and do nothing unless consumed"] - pub fn query_neighbors>( + pub fn query_neighbors>( &self, particle: Q, ) -> Option + Clone> { @@ -436,7 +436,7 @@ where impl CellGrid where T: Float + NumAssignOps + ConstOne + AsPrimitive + Send + Sync + std::fmt::Debug, - P: Particle<[T; N]> + Send + Sync, + P: ParticleLike<[T; N]> + Send + Sync, { /// Returns a parallel iterator over all relevant (i.e. within cutoff threshold + some extra) /// unique pairs of particles in this `CellGrid`. diff --git a/src/cellgrid/flatindex.rs b/src/cellgrid/flatindex.rs index b27d9d9..beeaefc 100644 --- a/src/cellgrid/flatindex.rs +++ b/src/cellgrid/flatindex.rs @@ -1,7 +1,7 @@ //TODO: currently assuming that the order of points in point cloud does not change //TODO: i.e. index in flatindex corresponds to index in point cloud, this should be documented use super::util::{Aabb, GridInfo}; -use crate::Particle; +use crate::ParticleLike; use itertools::Itertools; use nalgebra::SimdPartialOrd; use num_traits::{AsPrimitive, ConstOne, ConstZero, Float, NumAssignOps}; @@ -73,7 +73,7 @@ where //TODO: see https://www.rustsim.org/blog/2020/03/23/simd-aosoa-in-nalgebra/#using-simd-aosoa-for-linear-algebra-in-rust-ultraviolet-and-nalgebra //TODO: or can I chunk iterators such that rustc auto-vectorizes? //TODO: see https://www.nickwilcox.com/blog/autovec/ - pub fn from_particles>( + pub fn from_particles>( particles: impl IntoIterator> + Clone, cutoff: F, ) -> Self { @@ -110,7 +110,7 @@ where // there is no rebuild(), named it rebuild_mut() to match CellGrid::rebuild_mut() // TODO: Documentation: return bool indicating whether the index changed at all (in length or any individual entry) // TODO: benchmark with changing point iterators - pub fn rebuild_mut>( + pub fn rebuild_mut>( &mut self, particles: impl IntoIterator> + Clone, cutoff: Option, diff --git a/src/cellgrid/iters.rs b/src/cellgrid/iters.rs index 15871fa..652a695 100644 --- a/src/cellgrid/iters.rs +++ b/src/cellgrid/iters.rs @@ -3,7 +3,7 @@ use crate::cellgrid::storage::CellSliceMeta; #[cfg(feature = "rayon")] use crate::rayon::ParallelIterator; -use crate::{CellGrid, Particle}; +use crate::{CellGrid, ParticleLike}; use core::iter::FusedIterator; use core::slice::Iter; use itertools::Itertools; @@ -95,7 +95,7 @@ pub mod neighborhood { pub struct GridCell<'g, P, const N: usize = 3, F: Float = f64> where F: NumAssignOps + ConstOne + AsPrimitive + std::fmt::Debug, - P: Particle<[F; N]>, + P: ParticleLike<[F; N]>, { //TODO: maybe provide proper accessors to these fields for neighbors.rs to use? //TODO: is there a better way than having a reference to the containing CellGrid? @@ -106,7 +106,7 @@ where impl<'g, P, const N: usize, F> GridCell<'g, P, N, F> where F: Float + NumAssignOps + ConstOne + AsPrimitive + Send + Sync + std::fmt::Debug, - P: Particle<[F; N]> + Send + Sync, + P: ParticleLike<[F; N]> + Send + Sync, { /// Returns the (flat) cell index of this (possibly empty) `GridCell`. pub(crate) fn index(&self) -> i32 { @@ -218,7 +218,7 @@ where impl CellGrid where F: Float + NumAssignOps + ConstOne + AsPrimitive + Send + Sync + std::fmt::Debug, - P: Particle<[F; N]>, + P: ParticleLike<[F; N]>, { /// Returns an iterator over all [`GridCell`]s in this `CellGrid`, excluding empty cells. /// diff --git a/src/cellgrid/util.rs b/src/cellgrid/util.rs index 20d6c19..0f15308 100644 --- a/src/cellgrid/util.rs +++ b/src/cellgrid/util.rs @@ -1,5 +1,5 @@ //! Several utility items that might be useful but usually do not need be interacted with. -use crate::Particle; +use crate::ParticleLike; use nalgebra::{Point, SVector, SimdPartialOrd}; use num_traits::{AsPrimitive, ConstOne, ConstZero, Float, NumAssignOps}; #[cfg(feature = "serde")] @@ -32,7 +32,7 @@ where { /// Computes the componentwise minimum and maximum from the coordinates /// of the supplied particle data. - pub fn from_particles>( + pub fn from_particles>( mut particles: impl Iterator>, ) -> Self { let init = particles @@ -52,7 +52,7 @@ where } //TODO: could also pass iterators here (single point could be wrapped by std::iter::once or Option::iter()) - fn update>(&mut self, particle: impl Borrow

) { + fn update>(&mut self, particle: impl Borrow

) { let p = Point::from(particle.borrow().coords()); self.inf = p.inf(&self.inf); self.sup = p.sup(&self.sup); diff --git a/src/lib.rs b/src/lib.rs index 0859e1d..0ae06e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,8 +83,9 @@ pub use crate::cellgrid::CellGrid; /// Only [`Copy`] types can be used. /// In general, the smaller the type, the better (for the CPU cache). /// +// FIXME: update this doc section /// A blanket implementation for `Into + Copy` types is provided.\ -/// [`CellGrid`] is slightly more specific and requires implementing `Particle<[{float}; N]>`. +/// [`CellGrid`] is slightly more specific and requires implementing `ParticleLike<[{float}; N]>`. /// Therefore, fixed-size float arrays, [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types that can be `Deref`-coerced /// into the former or [`mint`](https://docs.rs/mint/latest/mint/) types, can be directly used. /// @@ -94,7 +95,7 @@ pub use crate::cellgrid::CellGrid; /// /// # Examples /// ``` -/// # use zelll::Particle; +/// # use zelll::ParticleLike; /// # #[derive(Clone, Copy)] /// # enum Element { /// # Hydrogen, // no associated coordinate data since it's the same for all variants @@ -109,40 +110,18 @@ pub use crate::cellgrid::CellGrid; /// kind: Element, /// coords: [f64; 3], /// } -/// impl Particle for Atom { +/// impl ParticleLike for Atom { /// #[inline] /// fn coords(&self) -> [f64; 3] { /// self.coords // no pattern matching required here /// } /// } /// ``` -pub trait Particle: Copy { +pub trait ParticleLike: Copy { /// Returns a copy of this particle's coordinates. fn coords(&self) -> T; } -// TODO: Might consider restricting this impl. -// TODO: While we can be this generic, this might help articulating our intentions better: -// TODO: impl Particle<[T; N]> for P where P: Into<[T; N]> + Copy { -// impl Particle<[T; N]> for P -// where -// P: Into<[T; N]> + Copy, -// { -// #[inline] -// fn coords(&self) -> [T; N] { -//

>::into(*self) -// } -// } - -// #[derive(Copy, Clone)] -// pub struct LabeledParticle(L, T); - -// impl> Particle for LabeledParticle { -// fn coords(&self) -> T { -// self.1.coords() -// } -// } - // TODO: maybe name this Particle, and the trait should be ParticleLike? // TODO: could have additional trait alias IntoParticle: Into<[T; N]> + Copy where T: Copy? // TODO: then blanket impl ParticleLike on IntoParticle? is that possible? @@ -157,9 +136,9 @@ pub struct WrappedParticle

{ inner: P, } -impl Particle<[T; N]> for WrappedParticle

+impl ParticleLike<[T; N]> for WrappedParticle

where - // P: Particle, + // P: ParticleLike, P: Into<[T; N]> + Copy, { fn coords(&self) -> [T; N] { @@ -184,10 +163,10 @@ impl

From

for WrappedParticle

{ } } -impl Particle<[T; N]> for (usize, P) +impl ParticleLike<[T; N]> for (usize, P) where /* P: Into<[T; N]> + Copy, */ - P: Particle<[T; N]>, + P: ParticleLike<[T; N]>, { #[inline] fn coords(&self) -> [T; N] /* [T; N] */ { @@ -196,7 +175,7 @@ where } } -impl Particle<[T; N]> for [T; N] +impl ParticleLike<[T; N]> for [T; N] where T: Copy, { @@ -264,7 +243,7 @@ mod tests { fn convert(&self) -> Vec where - P: Particle, + P: ParticleLike, { self.buffer.iter().map(|p| p.coords()).collect() } @@ -324,7 +303,7 @@ mod tests { #[derive(Clone, Copy)] struct ParticleRef<'p>(&'p [f64; 3]); - impl Particle<[f64; 3]> for ParticleRef<'_> { + impl ParticleLike<[f64; 3]> for ParticleRef<'_> { #[inline] fn coords(&self) -> [f64; 3] { (*self.0).coords() // equivalent to *self.0 diff --git a/surface-sampling/src/sdf/numdual.rs b/surface-sampling/src/sdf/numdual.rs index fddbfa9..84627ff 100644 --- a/surface-sampling/src/sdf/numdual.rs +++ b/surface-sampling/src/sdf/numdual.rs @@ -2,7 +2,7 @@ use crate::Angstrom; use crate::sdf::SmoothDistanceField; use nalgebra::{ComplexField, SVector}; use num_dual::*; -use zelll::Particle; +use zelll::ParticleLike; impl SmoothDistanceField { // TODO: this actually looks cleaner with a for loop... From 3bd6622292483b999a2adde084e5118aef810804 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 10:34:38 +0100 Subject: [PATCH 03/11] API: Rename struct WrappedParticle to Particle --- examples/cachemisses.rs | 6 ++--- examples/minimal.rs | 6 ++--- src/cellgrid.rs | 7 +++--- src/lib.rs | 44 ++++++++++++++----------------------- surface-sampling/src/sdf.rs | 8 +++---- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/examples/cachemisses.rs b/examples/cachemisses.rs index 9ab26df..8b98a86 100644 --- a/examples/cachemisses.rs +++ b/examples/cachemisses.rs @@ -8,7 +8,7 @@ use crabgrind::callgrind as valgrind; use nalgebra::{Point, Point3, Vector3}; use rand::distributions::Standard; use rand::prelude::*; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; type PointCloud = Vec>; /// Generate a uniformly random 3D point cloud of size `n` in a cuboid of edge lengths `vol` centered around `origin`. @@ -68,7 +68,7 @@ fn main() { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); } @@ -80,7 +80,7 @@ fn main() { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); } diff --git a/examples/minimal.rs b/examples/minimal.rs index b1f11c0..bb2d1f4 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -6,7 +6,7 @@ use rand::prelude::*; #[cfg(feature = "rayon")] use rayon::prelude::ParallelIterator; use std::hint::black_box; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; type PointCloud = Vec>; /// Generate a uniformly random 3D point cloud of size `n` in a cuboid of edge lengths `vol` centered around `origin`. @@ -42,7 +42,7 @@ fn main() { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); println!("{:?}", cg.info().shape()); @@ -60,7 +60,7 @@ fn main() { #[cfg(feature = "rayon")] cg.par_particle_pairs() - .filter(|&((_i, p), (_j, q))| distance_squared(&p.into(), &q.into()) <= _cutoff_squared) + .filter(|&((_i, p), (_j, q))| distance_squared(&(*p).into(), &(*q).into()) <= _cutoff_squared) .for_each(|_| { //count += 1; black_box(()); diff --git a/src/cellgrid.rs b/src/cellgrid.rs index a1b95ea..2042f2d 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -98,13 +98,14 @@ pub use util::{Aabb, GridInfo}; /// let data: Vec<[f32; 2]> = vec![[0.0, 0.0], [1.0,2.0], [0.0, 0.1]]; /// let mut cg = CellGrid::new(data.iter().copied(), 1.0); /// ``` -/// Any type implementing [`Particle`] can be used: +/// Any type implementing [`ParticleLike`] can be used: +// FIXME: adjust this documentation, it's more about the wrapper type now /// ``` -/// # use zelll::{CellGrid, WrappedParticle}; +/// # use zelll::{CellGrid, Particle}; /// use nalgebra::SVector; /// /// let data: Vec> = vec![[0.0, 0.0].into(), [1.0,2.0].into(), [0.0, 0.1].into()]; -/// let mut cg = CellGrid::new(data.iter().copied().map(WrappedParticle::from), 1.0); +/// let mut cg = CellGrid::new(data.iter().copied().map(Particle::from), 1.0); /// ``` #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/lib.rs b/src/lib.rs index 0ae06e4..5d5431f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,7 +83,7 @@ pub use crate::cellgrid::CellGrid; /// Only [`Copy`] types can be used. /// In general, the smaller the type, the better (for the CPU cache). /// -// FIXME: update this doc section +// FIXME: update this doc section/move example to Particle

/// A blanket implementation for `Into + Copy` types is provided.\ /// [`CellGrid`] is slightly more specific and requires implementing `ParticleLike<[{float}; N]>`. /// Therefore, fixed-size float arrays, [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types that can be `Deref`-coerced @@ -122,34 +122,27 @@ pub trait ParticleLike: Copy { fn coords(&self) -> T; } -// TODO: maybe name this Particle, and the trait should be ParticleLike? // TODO: could have additional trait alias IntoParticle: Into<[T; N]> + Copy where T: Copy? // TODO: then blanket impl ParticleLike on IntoParticle? is that possible? // TODO: alternative: IntoParticle could be somewhat object-safe in the sense that it has -// TODO: a trait method returning (formerly Wrapped)Particle

+// TODO: a trait method returning Particle

// TODO: maybe it would be object-safe it has an associated type instead of parameter P -// TODO: if I make (Wrapped)Particle a tuple struct again, we wouldn't need the from impl anymore -// TODO: which would turn .map(WrappedParticle::from) into .map(WrappedParticle) -// TODO: ( or rather .map(Particle) after renaming) #[derive(Copy, Clone, Debug, Default)] -pub struct WrappedParticle

{ +pub struct Particle

{ inner: P, } -impl ParticleLike<[T; N]> for WrappedParticle

+impl ParticleLike<[T; N]> for Particle

where - // P: ParticleLike, P: Into<[T; N]> + Copy, { + #[inline] fn coords(&self) -> [T; N] { - // self.inner.coords()

>::into(self.inner) } } -use std::ops::Deref; - -impl

Deref for WrappedParticle

{ +impl

std::ops::Deref for Particle

{ type Target = P; fn deref(&self) -> &Self::Target { @@ -157,7 +150,7 @@ impl

Deref for WrappedParticle

{ } } -impl

From

for WrappedParticle

{ +impl

From

for Particle

{ fn from(value: P) -> Self { Self { inner: value } } @@ -165,12 +158,10 @@ impl

From

for WrappedParticle

{ impl ParticleLike<[T; N]> for (usize, P) where - /* P: Into<[T; N]> + Copy, */ P: ParticleLike<[T; N]>, { #[inline] - fn coords(&self) -> [T; N] /* [T; N] */ { - /*

>::into(self.1) */ + fn coords(&self) -> [T; N] { self.1.coords() } } @@ -251,10 +242,10 @@ mod tests { #[test] fn test_wrapped_particle() { - // let x: WrappedParticle<_> = [0.0f64; 3].into(); - let x = WrappedParticle::from([0.0f64; 3]); - x.coords(); // calls impl Particle for WrappedParticle

- (*x).coords(); // calls implParticle for P where P: Particle + // let x: Particle<_> = [0.0f64; 3].into(); + let x = Particle::from([0.0f64; 3]); + x.coords(); // calls impl ParticleLike for Particle

+ (*x).coords(); // calls impl ParticleLike for P where P: ParticleLike let points = std::iter::repeat_n(x, 10); @@ -266,18 +257,15 @@ mod tests { let _ = points.clone().enumerate().map(|ix| ix.coords()); let _ = points.enumerate().map(|(_i, x)| x.coords()); - let points = std::iter::repeat_n(SVector::from([0.0f64; 3]), 10); + let points = std::iter::repeat_n(SVector::from([0.0f64; 3]), 10) + .map(Particle::from) + .enumerate(); let _ = points .clone() - .map(WrappedParticle::from) - .enumerate() .map(|ix| -> [f64; 3] { ix.coords() }); let _ = points - .clone() - .map(WrappedParticle::from) - .enumerate() .map(|(_i, x)| -> [f64; 3] { x.coords() }); } @@ -291,7 +279,7 @@ mod tests { let points: Vec<_> = points .into_iter() .map(SVector::from) - .map(WrappedParticle::from) + .map(Particle::from) .collect(); let ps = PStorage::new_sparse(points.iter().copied()); diff --git a/surface-sampling/src/sdf.rs b/surface-sampling/src/sdf.rs index d6cecca..e2a0c18 100644 --- a/surface-sampling/src/sdf.rs +++ b/surface-sampling/src/sdf.rs @@ -7,11 +7,11 @@ pub use numdual::*; use crate::Angstrom; use crate::atom::Atom; use crate::io::PointCloud; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; #[derive(Clone, Debug)] pub struct SmoothDistanceField { - inner: CellGrid, 3, Angstrom>, + inner: CellGrid, 3, Angstrom>, pub(crate) surface_radius: Angstrom, pub(crate) k_force: Angstrom, // this wouldn't survive dimensional analysis though } @@ -20,7 +20,7 @@ impl SmoothDistanceField { pub fn new(protein: &PointCloud, cutoff: Angstrom) -> Self { Self { inner: CellGrid::new( - protein.points.iter().copied().map(WrappedParticle::from), + protein.points.iter().copied().map(Particle::from), cutoff, ), surface_radius: 1.05, @@ -42,7 +42,7 @@ impl SmoothDistanceField { Self { k_force, ..self } } - pub fn grid(&self) -> &CellGrid, 3, Angstrom> { + pub fn grid(&self) -> &CellGrid, 3, Angstrom> { &self.inner } } From d0825a5cb1db1ec8e63e52480826bd6b2806ad4f Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 10:36:47 +0100 Subject: [PATCH 04/11] Chore: cargo fmt --- examples/cachemisses.rs | 10 ++-------- examples/minimal.rs | 9 ++++----- src/lib.rs | 7 ++----- surface-sampling/src/sdf.rs | 5 +---- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/examples/cachemisses.rs b/examples/cachemisses.rs index 8b98a86..85adaba 100644 --- a/examples/cachemisses.rs +++ b/examples/cachemisses.rs @@ -65,10 +65,7 @@ fn main() { valgrind::start_instrumentation(); for _ in 0..repeat { let _cg = CellGrid::new( - pointcloud - .iter() - .map(|p| p.coords) - .map(Particle::from), + pointcloud.iter().map(|p| p.coords).map(Particle::from), cutoff, ); } @@ -77,10 +74,7 @@ fn main() { valgrind::start_instrumentation(); for _ in 0..repeat { let _cg = CellGrid::new( - pointcloud - .iter() - .map(|p| p.coords) - .map(Particle::from), + pointcloud.iter().map(|p| p.coords).map(Particle::from), cutoff, ); } diff --git a/examples/minimal.rs b/examples/minimal.rs index bb2d1f4..926959a 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -39,10 +39,7 @@ fn main() { //pointcloud.sort_unstable_by(|p, q| p.z.partial_cmp(&q.z).unwrap()); let cg = CellGrid::new( - pointcloud - .iter() - .map(|p| p.coords) - .map(Particle::from), + pointcloud.iter().map(|p| p.coords).map(Particle::from), cutoff, ); println!("{:?}", cg.info().shape()); @@ -60,7 +57,9 @@ fn main() { #[cfg(feature = "rayon")] cg.par_particle_pairs() - .filter(|&((_i, p), (_j, q))| distance_squared(&(*p).into(), &(*q).into()) <= _cutoff_squared) + .filter(|&((_i, p), (_j, q))| { + distance_squared(&(*p).into(), &(*q).into()) <= _cutoff_squared + }) .for_each(|_| { //count += 1; black_box(()); diff --git a/src/lib.rs b/src/lib.rs index 5d5431f..f24fec3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -261,12 +261,9 @@ mod tests { .map(Particle::from) .enumerate(); - let _ = points - .clone() - .map(|ix| -> [f64; 3] { ix.coords() }); + let _ = points.clone().map(|ix| -> [f64; 3] { ix.coords() }); - let _ = points - .map(|(_i, x)| -> [f64; 3] { x.coords() }); + let _ = points.map(|(_i, x)| -> [f64; 3] { x.coords() }); } #[test] diff --git a/surface-sampling/src/sdf.rs b/surface-sampling/src/sdf.rs index e2a0c18..bbd26ca 100644 --- a/surface-sampling/src/sdf.rs +++ b/surface-sampling/src/sdf.rs @@ -19,10 +19,7 @@ pub struct SmoothDistanceField { impl SmoothDistanceField { pub fn new(protein: &PointCloud, cutoff: Angstrom) -> Self { Self { - inner: CellGrid::new( - protein.points.iter().copied().map(Particle::from), - cutoff, - ), + inner: CellGrid::new(protein.points.iter().copied().map(Particle::from), cutoff), surface_radius: 1.05, k_force: 10.0, } From 32077fcc3f671878cb5527e46cee2fd62aba61b1 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 11:01:27 +0100 Subject: [PATCH 05/11] Fix: broken tests, benches, etc. due to API changes --- benches/cellgrid.rs | 6 +++--- benches/iters.rs | 4 ++-- benches/lj.rs | 6 +++--- surface-sampling/examples/cli.rs | 3 +-- surface-sampling/src/atom.rs | 2 +- surface-sampling/src/sdf/numdual.rs | 5 +++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/benches/cellgrid.rs b/benches/cellgrid.rs index ba9a197..b18acca 100644 --- a/benches/cellgrid.rs +++ b/benches/cellgrid.rs @@ -7,7 +7,7 @@ use rand::distributions::Standard; use rand::prelude::*; #[cfg(feature = "rayon")] use zelll::rayon::ParallelIterator; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; type F32or64 = f64; @@ -57,7 +57,7 @@ pub fn bench_cellgrid(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); @@ -70,7 +70,7 @@ pub fn bench_cellgrid(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ) }) diff --git a/benches/iters.rs b/benches/iters.rs index 0157cf7..1329824 100644 --- a/benches/iters.rs +++ b/benches/iters.rs @@ -10,7 +10,7 @@ use rand::prelude::*; use rayon::ThreadPoolBuilder; #[cfg(feature = "rayon")] use zelll::rayon::ParallelIterator; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; type F32or64 = f64; @@ -60,7 +60,7 @@ pub fn bench_iters(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); diff --git a/benches/lj.rs b/benches/lj.rs index 1278704..753dc54 100644 --- a/benches/lj.rs +++ b/benches/lj.rs @@ -6,7 +6,7 @@ use criterion::{ use nalgebra::{Point, Point3, Vector3, distance_squared}; use rand::distributions::Standard; use rand::prelude::*; -use zelll::{CellGrid, WrappedParticle}; +use zelll::{CellGrid, Particle}; type F32or64 = f64; @@ -74,7 +74,7 @@ pub fn bench_lj(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); let potential_energy: F32or64 = cg @@ -102,7 +102,7 @@ pub fn bench_lj(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(WrappedParticle::from), + .map(Particle::from), cutoff, ); let _potential_energy: F32or64 = cg diff --git a/surface-sampling/examples/cli.rs b/surface-sampling/examples/cli.rs index b493112..3636468 100644 --- a/surface-sampling/examples/cli.rs +++ b/surface-sampling/examples/cli.rs @@ -6,7 +6,6 @@ use psssh::io::PointCloud; use psssh::sdf::SmoothDistanceField; use std::path::PathBuf; use std::time::Instant; -use zelll::{Particle, WrappedParticle}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -109,7 +108,7 @@ fn main() { let init = data .points .first() - .map_or([0.0; 3], |atom| WrappedParticle::from(atom).coords()); + .map_or([0.0; 3], |atom| atom.coords); sampler .set_position(init.as_slice()) diff --git a/surface-sampling/src/atom.rs b/surface-sampling/src/atom.rs index cbacda0..122ea10 100644 --- a/surface-sampling/src/atom.rs +++ b/surface-sampling/src/atom.rs @@ -33,7 +33,7 @@ pub struct Atom { pub coords: [Angstrom; 3], } -// The blanket implementation for Particle can use this +// This allows Atom to be wrapped in zelll::Particle impl From for [Angstrom; 3] { fn from(atom: Atom) -> Self { atom.coords diff --git a/surface-sampling/src/sdf/numdual.rs b/surface-sampling/src/sdf/numdual.rs index 84627ff..72b0e32 100644 --- a/surface-sampling/src/sdf/numdual.rs +++ b/surface-sampling/src/sdf/numdual.rs @@ -108,7 +108,7 @@ impl SmoothDistanceField { mod tests { use super::*; use crate::atom::{Atom, Element}; - use zelll::CellGrid; + use zelll::{CellGrid, Particle}; // TODO: should use `approx` for this #[test] @@ -169,7 +169,8 @@ mod tests { points.iter().map(|&coords| Atom { element: Element::default(), coords, - }), + }) + .map(Particle::from), 1.0, ), surface_radius: 1.05, From 3560957414e2a11189399dfe007f97d41fb2e555 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 11:19:01 +0100 Subject: [PATCH 06/11] API: make impl ParticleLike for (_, P) more generic and fix broken reference in docs --- src/cellgrid.rs | 2 +- src/lib.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cellgrid.rs b/src/cellgrid.rs index 2042f2d..cfb6841 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -335,7 +335,7 @@ where /// The `Item` type is currently `((usize, P), (usize, P))`, /// where each particle is labelled with its position it was inserted into this `CellGrid`. /// A future breaking change might remove this label (think _entity ID_), putting the responsibility to associate `P` - /// with additional data (eg. velocities, momenta) onto implementors of [`Particle`]. + /// with additional data (eg. velocities, momenta) onto implementors of [`ParticleLike`]. /// This will likely also deprecate [`pair_indices()`](CellGrid::pair_indices()). /// /// diff --git a/src/lib.rs b/src/lib.rs index f24fec3..e11cc2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,8 +156,9 @@ impl

From

for Particle

{ } } -impl ParticleLike<[T; N]> for (usize, P) +impl ParticleLike<[T; N]> for (L, P) where + L: Copy, P: ParticleLike<[T; N]>, { #[inline] From a0076321d4195dca810f9c4dffc77cb56016b974 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 13:27:35 +0100 Subject: [PATCH 07/11] Doc: examples and API changes --- src/cellgrid.rs | 5 ++-- src/lib.rs | 76 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/cellgrid.rs b/src/cellgrid.rs index cfb6841..2da776c 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -98,14 +98,15 @@ pub use util::{Aabb, GridInfo}; /// let data: Vec<[f32; 2]> = vec![[0.0, 0.0], [1.0,2.0], [0.0, 0.1]]; /// let mut cg = CellGrid::new(data.iter().copied(), 1.0); /// ``` -/// Any type implementing [`ParticleLike`] can be used: -// FIXME: adjust this documentation, it's more about the wrapper type now +/// Any type able to be wrapped in [`crate::Particle`] or implementing [`ParticleLike`] can be used: /// ``` /// # use zelll::{CellGrid, Particle}; /// use nalgebra::SVector; /// /// let data: Vec> = vec![[0.0, 0.0].into(), [1.0,2.0].into(), [0.0, 0.1].into()]; /// let mut cg = CellGrid::new(data.iter().copied().map(Particle::from), 1.0); +/// // the input data can be easily augmented with indices by enumerating: +/// let mut cg = CellGrid::new(data.iter().copied().map(Particle::from).enumerate(), 1.0); /// ``` #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/lib.rs b/src/lib.rs index e11cc2f..23ae243 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,8 +40,8 @@ //! //! While the main struct [`CellGrid`] is generic over dimension `N`, //! it is intended to be used with `N = 2` or `N = 3`. -//! Particle data represented as fixed-size arrays is supported without additional work.\ -//! Additionally, implementing [`Particle`] allows usage of custom types as particle data. +//! Data represented as fixed-size arrays is supported by wrapping in [`Particle`].\ +//! Additionally, implementing [`ParticleLike`] allows usage of custom types as particle data. //! This can be used to encode different kinds of particles or enable interior mutability if required. //! //! # Examples @@ -83,11 +83,15 @@ pub use crate::cellgrid::CellGrid; /// Only [`Copy`] types can be used. /// In general, the smaller the type, the better (for the CPU cache). /// -// FIXME: update this doc section/move example to Particle

-/// A blanket implementation for `Into + Copy` types is provided.\ -/// [`CellGrid`] is slightly more specific and requires implementing `ParticleLike<[{float}; N]>`. -/// Therefore, fixed-size float arrays, [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types that can be `Deref`-coerced -/// into the former or [`mint`](https://docs.rs/mint/latest/mint/) types, can be directly used. +/// Note that [`CellGrid`] is more specific than this trait and requires implementing `ParticleLike<[{float}; N]>`. +/// +/// We do not provide a blanket implementation for types implementing `Into<[T; N]> + Copy` but +/// a wrapper type instead. +/// Therefore, [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types that can be `Deref`-coerced +/// into the former or [`mint`](https://docs.rs/mint/latest/mint/) types, can be used +/// by wrapping them in [`Particle`]. +/// +/// For convenience, this trait is implemented for `[{float}; N]` and key-value tuples. /// /// Having custom types implement this trait allows for patterns like interior mutability, /// referencing separate storage (e.g. with ECS, or concurrent storage types), @@ -122,11 +126,22 @@ pub trait ParticleLike: Copy { fn coords(&self) -> T; } -// TODO: could have additional trait alias IntoParticle: Into<[T; N]> + Copy where T: Copy? -// TODO: then blanket impl ParticleLike on IntoParticle? is that possible? -// TODO: alternative: IntoParticle could be somewhat object-safe in the sense that it has -// TODO: a trait method returning Particle

-// TODO: maybe it would be object-safe it has an associated type instead of parameter P +/// Wrapper type that implements [`ParticleLike`] for types that are `Into<[T; N]> + Copy`. +/// +/// Notable types that can be used with `Particle` include `({float}, ...)`, +/// [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types +/// that can be `Deref`-coerced into the former or [`mint`](https://docs.rs/mint/latest/mint/) types, +/// respectively. +/// +/// `Particle` can be `Deref`-coerced into the wrapped type. +/// +/// # Examples +/// ``` +/// use zelll::{Particle, ParticleLike}; +/// +/// let x: Particle<_> = (0.0f64, 0.0f64, 0.0f64).into(); +/// x.coords(); +/// ``` #[derive(Copy, Clone, Debug, Default)] pub struct Particle

{ inner: P, @@ -142,6 +157,14 @@ where } } +/// # Examples +/// Wrapping an array is redundant but illustrates the `Deref` behavior: +/// ``` +/// # use zelll::{Particle, ParticleLike}; +/// let x = Particle::from([0.0f64; 3]); +/// x.coords(); // calls impl ParticleLike for Particle

+/// (*x).coords(); // calls impl ParticleLike for P where P: ParticleLike +/// ``` impl

std::ops::Deref for Particle

{ type Target = P; @@ -156,6 +179,35 @@ impl

From

for Particle

{ } } +/// Sometimes, it is useful to store indices alongside particle data. +/// This is easily facilitated by enumerating the iterator used to construct a `CellGrid`. +/// +/// Other types such as [`std::collections::HashMap`] can also be used +/// although it is less straight-forward +/// +/// # Examples +/// Here, `ip` is a tuple `(usize, P)`. +/// ``` +/// # use zelll::ParticleLike; +/// # let x = [0.0f64; 3]; +/// # let points: Vec<_> = std::iter::repeat_n(x, 10).collect(); +/// points.iter() +/// .copied() +/// .enumerate() +/// .map(|ip| ip.coords()); +/// ``` +/// ``` +/// # use zelll::ParticleLike; +/// # use std::collections::HashMap; +/// # let data = [("a", [0.0f64; 3]); 10]; +/// let points = HashMap::from(data); +/// // this iterator is consuming +/// points.into_iter() +/// .map(|kv| kv.coords()); +/// // after constructing a CellGrid +/// // and iterating over pairs, the particles have to be inserted +/// // into a hash map again to do any meaningful work +/// ``` impl ParticleLike<[T; N]> for (L, P) where L: Copy, From 9d8039e813d0da562eb57ba6879b86e268a2714f Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 14:42:10 +0100 Subject: [PATCH 08/11] API: Stop enumerating particles internally in CellGrid and remove (par_)pair_indices(). This is now covered by the new ParticleLike-related changes, which is much more flexible. --- CITATION.cff | 2 +- README.md | 4 +- benches/cellgrid.rs | 8 ++-- benches/iters.rs | 3 +- benches/lj.rs | 6 ++- examples/minimal.rs | 6 ++- python/src/lib.rs | 7 ++-- src/cellgrid.rs | 64 ++++++++--------------------- src/cellgrid/iters.rs | 14 +++---- src/lib.rs | 9 ++-- surface-sampling/examples/cli.rs | 5 +-- surface-sampling/src/sdf/numdual.rs | 14 ++++--- 12 files changed, 57 insertions(+), 85 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 69f5b32..7565604 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,7 @@ cff-version: 1.2.0 title: ' zelll: a fast, framework-free, and flexible implementation of the cell lists algorithm for the Rust programming language' message: >- If you use this software, please cite the research article - and a versioned Zenodo DOI if appropriate. + and a versioned Zenodo DOI where appropriate. type: software authors: - given-names: Vincent diff --git a/README.md b/README.md index 5a4f850..ed747ca 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,13 @@ The latest Python API is documented [here](https://microscopic-image-analysis.gi use zelll::CellGrid; let data = vec![[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; -let mut cg = CellGrid::new(data.iter().copied(), 1.0); +let mut cg = CellGrid::new(data.iter().copied().enumerate(), 1.0); for ((i, p), (j, q)) in cg.particle_pairs() { /* do some work */ } -cg.rebuild_mut(data.iter().copied(), Some(0.5)); +cg.rebuild_mut(data.iter().copied().enumerate(), Some(0.5)); ``` ### Benchmarks diff --git a/benches/cellgrid.rs b/benches/cellgrid.rs index b18acca..7af082c 100644 --- a/benches/cellgrid.rs +++ b/benches/cellgrid.rs @@ -57,7 +57,8 @@ pub fn bench_cellgrid(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(Particle::from), + .map(Particle::from) + .enumerate(), cutoff, ); @@ -67,10 +68,7 @@ pub fn bench_cellgrid(c: &mut Criterion) { |b, pointcloud| { b.iter(|| { CellGrid::new( - pointcloud - .iter() - .map(|p| p.coords) - .map(Particle::from), + pointcloud.iter().map(|p| p.coords).map(Particle::from), cutoff, ) }) diff --git a/benches/iters.rs b/benches/iters.rs index 1329824..a76cfb1 100644 --- a/benches/iters.rs +++ b/benches/iters.rs @@ -60,7 +60,8 @@ pub fn bench_iters(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(Particle::from), + .map(Particle::from) + .enumerate(), cutoff, ); diff --git a/benches/lj.rs b/benches/lj.rs index 753dc54..2f88e6d 100644 --- a/benches/lj.rs +++ b/benches/lj.rs @@ -74,7 +74,8 @@ pub fn bench_lj(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(Particle::from), + .map(Particle::from) + .enumerate(), cutoff, ); let potential_energy: F32or64 = cg @@ -102,7 +103,8 @@ pub fn bench_lj(c: &mut Criterion) { pointcloud .iter() .map(|p| p.coords) - .map(Particle::from), + .map(Particle::from) + .enumerate(), cutoff, ); let _potential_energy: F32or64 = cg diff --git a/examples/minimal.rs b/examples/minimal.rs index 926959a..a3bf3c9 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -39,7 +39,11 @@ fn main() { //pointcloud.sort_unstable_by(|p, q| p.z.partial_cmp(&q.z).unwrap()); let cg = CellGrid::new( - pointcloud.iter().map(|p| p.coords).map(Particle::from), + pointcloud + .iter() + .map(|p| p.coords) + .map(Particle::from) + .enumerate(), cutoff, ); println!("{:?}", cg.info().shape()); diff --git a/python/src/lib.rs b/python/src/lib.rs index 2120c62..85abd32 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -95,7 +95,7 @@ impl<'py> Iterator for ParticlesIterator<'py> { #[derive(Clone)] #[pyclass(name = "CellGrid", module = "zelll")] pub struct PyCellGrid { - inner: CellGrid<[f64; 3]>, + inner: CellGrid<(usize, [f64; 3])>, } #[pymethods] @@ -117,7 +117,7 @@ impl PyCellGrid { let particles = ParticlesIterable { inner: p.as_borrowed(), }; - CellGrid::new(particles, cutoff) + CellGrid::new(particles.into_iter().enumerate(), cutoff) } // nutpie+multithreading needs to serialize/deserialize PyCellGrid // and the latter calls ::new() (albeit with default arguments) @@ -161,7 +161,8 @@ impl PyCellGrid { inner: particles.as_borrowed(), }; - slf.inner.rebuild_mut(particles, cutoff); + slf.inner + .rebuild_mut(particles.into_iter().enumerate(), cutoff); } fn __iter__(slf: PyRef<'_, Self>) -> PyCellGridIter { diff --git a/src/cellgrid.rs b/src/cellgrid.rs index 2da776c..e13393f 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -120,7 +120,7 @@ where // TODO: rebuild from CellStorage iterator (boils down to counting/bucket sorting) // TODO: iterate (mutably) over cell storage, iterate mutably over particle pairs // TODO: make it responsibility of user to associate some index/label with P: Particle? - cell_lists: CellStorage<(usize, P)>, + cell_lists: CellStorage

, index: FlatIndex, } @@ -151,18 +151,21 @@ where /// # use zelll::CellGrid; /// let data = vec![[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; /// let cell_grid = CellGrid::new(data.iter().copied(), 1.0); + /// // some examples iterate over enumerated particles: + /// let cell_grid = CellGrid::new(data.iter().copied().enumerate(), 1.0); /// ``` /// ///

/// /// Note that the intended usage includes `.iter().copied()` - /// because we require the items of `particles` to implement `Particle` and we do not - /// provide a blanket implementation for references to types implementing `Particle`. + /// (and `.enumerate()` if preferable) + /// because we require the items of `particles` to implement `ParticleLike` and we do not + /// provide a blanket implementation for references to types implementing `ParticleLike`. /// /// Previously we required `::Item: Borrow

` /// which provided more flexibility by accepting both `P` and `&P` /// but this forces lots of type annotations on the user.\ - /// Since `Particle: Copy` anyway, we sacrifice this flexibility + /// Since `ParticleLike: Copy` anyway, we sacrifice this flexibility /// in favor of not cluttering user code with type annotations. /// ///

@@ -224,15 +227,14 @@ where .index .iter() .zip(particles) - .enumerate() // TODO: we know cells.get_mut() won't fail by construction // TODO: but maybe use try_for_each() instead - .for_each(|(i, (cell, particle))| { + .for_each(|(cell, particle)| { // FIXME: in principle could have multiple &mut slices into CellStorage (for parallel pushing) // FIXME: would just have to make sure that cell is always unique when operating on chunks // FIXME: (pretty much the same issue as with counting cell sizes concurrently) cell_lists.push( - (i, particle), + particle, cells .get_mut(cell) .expect("cell grid should contain every cell in the grid index"), @@ -309,11 +311,10 @@ where .index .iter() .zip(particles) - .enumerate() //TODO: see `::rebuild()` - .for_each(|(i, (cell, particle))| { + .for_each(|(cell, particle)| { self.cell_lists.push( - (i, particle), + particle, self.cells .get_mut(cell) .expect("cell grid should contain every cell in the grid index"), @@ -331,22 +332,12 @@ where /// Returns an iterator over all relevant (i.e. within cutoff threshold + some extra) unique /// pairs of particles in this `CellGrid`. /// - ///
- /// - /// The `Item` type is currently `((usize, P), (usize, P))`, - /// where each particle is labelled with its position it was inserted into this `CellGrid`. - /// A future breaking change might remove this label (think _entity ID_), putting the responsibility to associate `P` - /// with additional data (eg. velocities, momenta) onto implementors of [`ParticleLike`]. - /// This will likely also deprecate [`pair_indices()`](CellGrid::pair_indices()). - /// - ///
- /// /// # Examples /// ``` /// # use zelll::CellGrid; /// use nalgebra::distance_squared; /// # let data = [[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; - /// # let cell_grid = CellGrid::new(data.iter().copied(), 1.0); + /// let cell_grid = CellGrid::new(data.iter().copied().enumerate(), 1.0); /// cell_grid.particle_pairs() /// // usually, .filter_map() is preferable (so distance computations can be re-used) /// .filter(|&((_i, p), (_j, q))| { @@ -357,21 +348,10 @@ where /// }); /// ``` #[must_use = "iterators are lazy and do nothing unless consumed"] - pub fn particle_pairs(&self) -> impl Iterator + Clone { + pub fn particle_pairs(&self) -> impl Iterator + Clone { self.iter().flat_map(|cell| cell.particle_pairs()) } - /// Returns an iterator over all relevant (i.e. within cutoff threshold + some extra) unique - /// pairs of particle indices in this `CellGrid`. - /// - /// A particle index is its position in the iterator that was used for constructing or rebuilding a `CellGrid`. - #[must_use = "iterators are lazy and do nothing unless consumed"] - pub fn pair_indices(&self) -> impl Iterator + Clone { - self.iter() - .flat_map(|cell| cell.particle_pairs()) - .map(|((i, _p), (j, _q))| (i, j)) - } - /// Returns spatial information about this cell grid, as well as auxiliary functionality /// facilitated by this information. /// @@ -408,7 +388,7 @@ where /// # use zelll::CellGrid; /// use nalgebra::distance_squared; /// # let data = [[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; - /// # let cell_grid = CellGrid::new(data.iter().copied(), 1.0); + /// let cell_grid = CellGrid::new(data.iter().copied().enumerate(), 1.0); /// let p = [0.5, 1.0, 0.1]; /// cell_grid.query_neighbors(p) /// .expect("the queried particle should be within `cutoff` of this grid's shape") @@ -424,7 +404,7 @@ where pub fn query_neighbors>( &self, particle: Q, - ) -> Option + Clone> { + ) -> Option + Clone> { self.query(particle).map(|this| { this.iter().copied().chain( this.neighbors::() @@ -454,7 +434,7 @@ where /// # use zelll::{CellGrid, rayon::ParallelIterator}; /// use nalgebra::distance_squared; /// # let data = [[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; - /// # let cell_grid = CellGrid::new(data.iter().copied(), 1.0); + /// let cell_grid = CellGrid::new(data.iter().copied().enumerate(), 1.0); /// cell_grid.par_particle_pairs() // TODO: fact-check the statement below: /// // Try to avoid filtering this ParallelIterator to avoid significant overhead: @@ -464,19 +444,9 @@ where /// } /// }); /// ``` - pub fn par_particle_pairs(&self) -> impl ParallelIterator { + pub fn par_particle_pairs(&self) -> impl ParallelIterator { // TODO: ideally, we would schedule 2 threads for cell.particle_pairs() with the same CPU affinity // TODO: so they can share their resources self.par_iter().flat_map_iter(|cell| cell.particle_pairs()) } - - /// Returns a parallel iterator over all relevant (i.e. within cutoff threshold + some extra) unique - /// pairs of particle indices in this `CellGrid`. - /// - /// A particle index is its position in the iterator that was used for constructing or rebuilding a `CellGrid`. - pub fn par_pair_indices(&self) -> impl ParallelIterator { - self.par_iter() - .flat_map_iter(|cell| cell.particle_pairs()) - .map(|((i, _p), (j, _q))| (i, j)) - } } diff --git a/src/cellgrid/iters.rs b/src/cellgrid/iters.rs index 652a695..f11fc23 100644 --- a/src/cellgrid/iters.rs +++ b/src/cellgrid/iters.rs @@ -115,10 +115,10 @@ where /// Returns an iterator over all particles in this `GridCell`. /// - /// The item type is a pair consisting of the particle index as iterated during `CellGrid` + // The item type is a pair consisting of the particle index as iterated during `CellGrid` /// construction and the particle data itself. // TODO: should probably rather impl IntoIterator to match consuming/copy behaviour of neighbors()/point_pairs()? - pub fn iter(self) -> Iter<'g, (usize, P)> { + pub fn iter(self) -> Iter<'g, P> { self.grid .cell_lists .cell_slice( @@ -182,9 +182,9 @@ where /// Returns an iterator over all unique pairs of points in this `GridCell`. #[inline] - fn intra_cell_pairs(self) -> impl FusedIterator + Clone { + fn intra_cell_pairs(self) -> impl FusedIterator + Clone { // this is equivalent to - // self.iter().copied().tuple_combinations::<((usize, P), (usize, P))>() + // self.iter().copied().tuple_combinations::<(P, P)>() // but faster for our specific case (pairs from slice of `Copy` values) self.iter() .copied() @@ -194,7 +194,7 @@ where /// Returns an iterator over all unique pairs of points in this `GridCell` with points of the neighboring cells. #[inline] - fn inter_cell_pairs(self) -> impl FusedIterator + Clone { + fn inter_cell_pairs(self) -> impl FusedIterator + Clone { self.iter().copied().cartesian_product( self.neighbors::() .flat_map(|cell| cell.iter().copied()), @@ -208,9 +208,7 @@ where /// This method consumes `self` but `GridCell` implements [`Copy`]. //TODO: handle full-space as well //TODO: document that we're relying on GridCell impl'ing Copy here (so we can safely consume `self`) - pub fn particle_pairs( - self, - ) -> impl FusedIterator + Clone + Send + Sync { + pub fn particle_pairs(self) -> impl FusedIterator + Clone + Send + Sync { self.intra_cell_pairs().chain(self.inter_cell_pairs()) } } diff --git a/src/lib.rs b/src/lib.rs index 23ae243..4e5937c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,13 +49,13 @@ //! use zelll::CellGrid; //! //! let data = vec![[0.0, 0.0, 0.0], [1.0,2.0,0.0], [0.0, 0.1, 0.2]]; -//! let mut cg = CellGrid::new(data.iter().copied(), 1.0); +//! let mut cg = CellGrid::new(data.iter().copied().enumerate(), 1.0); //! //! for ((i, p), (j, q)) in cg.particle_pairs() { //! /* do some work */ //! } //! -//! cg.rebuild_mut(data.iter().copied(), Some(0.5)); +//! cg.rebuild_mut(data.iter().copied().enumerate(), Some(0.5)); //! ``` //! //! [^etymology]: abbrv. from German _Zelllisten_ /ˈʦɛlɪstən/, for cell lists. @@ -129,9 +129,8 @@ pub trait ParticleLike: Copy { /// Wrapper type that implements [`ParticleLike`] for types that are `Into<[T; N]> + Copy`. /// /// Notable types that can be used with `Particle` include `({float}, ...)`, -/// [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), or types -/// that can be `Deref`-coerced into the former or [`mint`](https://docs.rs/mint/latest/mint/) types, -/// respectively. +/// [`nalgebra::SVector`](https://docs.rs/nalgebra/latest/nalgebra/base/type.SVector.html), +/// or [`mint`](https://docs.rs/mint/latest/mint/) types. /// /// `Particle` can be `Deref`-coerced into the wrapped type. /// diff --git a/surface-sampling/examples/cli.rs b/surface-sampling/examples/cli.rs index 3636468..4f0a4fb 100644 --- a/surface-sampling/examples/cli.rs +++ b/surface-sampling/examples/cli.rs @@ -105,10 +105,7 @@ fn main() { // .map_or([0.0; 3], |atom| atom.coords()); // TODO: alternatively, let's just use the first atom, assuming it's at one of the ends // TODO: we're discarding the first `b` samples anyway - let init = data - .points - .first() - .map_or([0.0; 3], |atom| atom.coords); + let init = data.points.first().map_or([0.0; 3], |atom| atom.coords); sampler .set_position(init.as_slice()) diff --git a/surface-sampling/src/sdf/numdual.rs b/surface-sampling/src/sdf/numdual.rs index 72b0e32..5aa3ec7 100644 --- a/surface-sampling/src/sdf/numdual.rs +++ b/surface-sampling/src/sdf/numdual.rs @@ -14,7 +14,7 @@ impl SmoothDistanceField { ) -> Option { let at: [D; 3] = x.into(); let at = at.map(|coord| coord.re()); - let neighbors = self.inner.query_neighbors(at)?.map(|(_, atom)| { + let neighbors = self.inner.query_neighbors(at)?.map(|atom| { let coords: [Angstrom; 3] = atom.coords(); let coords = coords.map(|c| c.into()); (atom.element.radius(), SVector::from(coords)) @@ -166,11 +166,13 @@ mod tests { let sdf = SmoothDistanceField { inner: CellGrid::new( - points.iter().map(|&coords| Atom { - element: Element::default(), - coords, - }) - .map(Particle::from), + points + .iter() + .map(|&coords| Atom { + element: Element::default(), + coords, + }) + .map(Particle::from), 1.0, ), surface_radius: 1.05, From ba85507c454ba9ec583136f33484a0d01970ff96 Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 17:04:47 +0100 Subject: [PATCH 09/11] Fix: PyO3 iteration broke due to Rust-side API changes --- python/src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/python/src/lib.rs b/python/src/lib.rs index 85abd32..0205119 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -5,6 +5,7 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PyBytes; use pyo3::types::PyIterator; +use std::iter::Enumerate; // TODO: While having a borrowing iterator is nice, it's expensive on Python's side // TODO: (every call to next() is an expensive python function call). @@ -17,24 +18,24 @@ struct ParticlesIterable<'a, 'py> { } impl<'a, 'py> IntoIterator for ParticlesIterable<'a, 'py> { - type Item = [f64; 3]; + type Item = (usize, [f64; 3]); type IntoIter = ParticlesIterator<'py>; fn into_iter(self) -> Self::IntoIter { ParticlesIterator { // PyO3 also just `unwrap()`s in their specific iterators - iter: self.inner.try_iter().unwrap(), + iter: self.inner.try_iter().unwrap().enumerate(), } } } #[derive(Clone)] struct ParticlesIterator<'py> { - iter: Bound<'py, PyIterator>, + iter: Enumerate>, } impl<'py> Iterator for ParticlesIterator<'py> { - type Item = [f64; 3]; + type Item = (usize, [f64; 3]); fn next(&mut self) -> Option { // TODO: document behavior: @@ -44,13 +45,13 @@ impl<'py> Iterator for ParticlesIterator<'py> { // if it's Some, attempt conversion and break loop if it was successful // otherwise, retry with next element loop { - match self.iter.next().transpose() { - Ok(Some(p)) => match <[f64; 3] as FromPyObject>::extract(p.as_borrowed()) { - Ok(p) => break Some(p), + match self.iter.next() { + Some((i, Ok(x))) => match <[f64; 3] as FromPyObject>::extract(x.as_borrowed()) { + Ok(p) => break Some((i, p)), Err(_) => (), }, - Ok(None) => break None, - Err(_) => (), + None => break None, + Some((_, Err(_))) => (), } } } @@ -117,7 +118,7 @@ impl PyCellGrid { let particles = ParticlesIterable { inner: p.as_borrowed(), }; - CellGrid::new(particles.into_iter().enumerate(), cutoff) + CellGrid::new(particles, cutoff) } // nutpie+multithreading needs to serialize/deserialize PyCellGrid // and the latter calls ::new() (albeit with default arguments) @@ -161,8 +162,7 @@ impl PyCellGrid { inner: particles.as_borrowed(), }; - slf.inner - .rebuild_mut(particles.into_iter().enumerate(), cutoff); + slf.inner.rebuild_mut(particles, cutoff); } fn __iter__(slf: PyRef<'_, Self>) -> PyCellGridIter { From 845f1982bd83a445eecb8a162828b787225fe5ab Mon Sep 17 00:00:00 2001 From: fncnt Date: Thu, 12 Feb 2026 17:09:37 +0100 Subject: [PATCH 10/11] Python: Update dep:pyo3 --- python/Cargo.toml | 2 +- python/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/Cargo.toml b/python/Cargo.toml index 7f599dd..657a41c 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib"] [dependencies] bincode = { version = "2.0", features = ["serde"] } -pyo3 = { version = "0.27", features = ["experimental-inspect"] } +pyo3 = { version = "0.28", features = ["experimental-inspect"] } zelll = { path = "..", features = ["serde"] } serde = { version = "1.0" } diff --git a/python/src/lib.rs b/python/src/lib.rs index 0205119..059a8b3 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -94,7 +94,7 @@ impl<'py> Iterator for ParticlesIterator<'py> { /// dist = np.linalg.norm(np.array(p) - np.array(q)) /// ``` #[derive(Clone)] -#[pyclass(name = "CellGrid", module = "zelll")] +#[pyclass(name = "CellGrid", module = "zelll", skip_from_py_object)] pub struct PyCellGrid { inner: CellGrid<(usize, [f64; 3])>, } @@ -401,7 +401,7 @@ impl PyCellQueryIter { /// for further technical details and background information. /// If in doubt, the Rust API docs should be considered the authoritative source for the exact behavior /// of the library. -#[pymodule(gil_used = false)] +#[pymodule] pub mod zelll { #[pymodule_export] pub use super::{PyCellGrid, PyCellGridIter, PyCellQueryIter}; From 17cc72d67c8fbbb016cd6ddc1e4a86992f2817c2 Mon Sep 17 00:00:00 2001 From: fncnt Date: Fri, 13 Feb 2026 10:43:01 +0100 Subject: [PATCH 11/11] API: add impl ParticleLike for references and update documentation accordingly --- src/cellgrid.rs | 16 +++++------- src/lib.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/cellgrid.rs b/src/cellgrid.rs index e13393f..5c6a46f 100644 --- a/src/cellgrid.rs +++ b/src/cellgrid.rs @@ -98,7 +98,8 @@ pub use util::{Aabb, GridInfo}; /// let data: Vec<[f32; 2]> = vec![[0.0, 0.0], [1.0,2.0], [0.0, 0.1]]; /// let mut cg = CellGrid::new(data.iter().copied(), 1.0); /// ``` -/// Any type able to be wrapped in [`crate::Particle`] or implementing [`ParticleLike`] can be used: +/// Any type that is able to be wrapped in [`Particle`](crate::Particle) +/// or implements [`ParticleLike`] can be used: /// ``` /// # use zelll::{CellGrid, Particle}; /// use nalgebra::SVector; @@ -157,16 +158,11 @@ where /// ///
/// - /// Note that the intended usage includes `.iter().copied()` - /// (and `.enumerate()` if preferable) - /// because we require the items of `particles` to implement `ParticleLike` and we do not - /// provide a blanket implementation for references to types implementing `ParticleLike`. + /// Here, the preferred usage includes `.iter().copied()` + /// but iteration by reference is supported. /// - /// Previously we required `::Item: Borrow

` - /// which provided more flexibility by accepting both `P` and `&P` - /// but this forces lots of type annotations on the user.\ - /// Since `ParticleLike: Copy` anyway, we sacrifice this flexibility - /// in favor of not cluttering user code with type annotations. + /// See [`ParticleLike`](trait.ParticleLike.html#foreign-impls) + /// for detailed information and examples. /// ///

pub fn new(particles: I, cutoff: T) -> Self diff --git a/src/lib.rs b/src/lib.rs index 4e5937c..6070cd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,15 @@ impl

From

for Particle

{ /// This is easily facilitated by enumerating the iterator used to construct a `CellGrid`. /// /// Other types such as [`std::collections::HashMap`] can also be used -/// although it is less straight-forward +/// although it is less straight-forward. +/// +///

+/// +/// See +/// [`impl ParticleLike<[T; N]> for &P`](#impl-ParticleLike%3C%5BT;+N%5D%3E-for-%26P) +/// for more information. +/// +///
/// /// # Examples /// Here, `ip` is a tuple `(usize, P)`. @@ -201,8 +209,7 @@ impl

From

for Particle

{ /// # let data = [("a", [0.0f64; 3]); 10]; /// let points = HashMap::from(data); /// // this iterator is consuming -/// points.into_iter() -/// .map(|kv| kv.coords()); +/// points.into_iter().map(|kv| kv.coords()); /// // after constructing a CellGrid /// // and iterating over pairs, the particles have to be inserted /// // into a hash map again to do any meaningful work @@ -228,6 +235,59 @@ where } } +/// References to `ParticleLike` types can also be used with +/// [`CellGrid`]. +/// +///

+/// +/// This trait `impl` aims to support collections like `HashMap` in an ergonomic way. +/// Usually, `CellGrid` is intended to be used without references to particle data, i.e. use +/// `.iter().copied()` where possible (and `.enumerate()` if preferable). +/// +///
+/// +/// # Examples +/// `HashMap::iter()` cannot be used with [`Iterator::copied()`]: +/// ```compile_fail +/// # use zelll::ParticleLike; +/// # use std::collections::HashMap; +/// let data = [("a", [0.0f64; 3]); 10]; +/// let points = HashMap::from(data); +/// // this iterator is not consuming the hash map +/// points.iter() +/// .copied() // this does not work on HashMap +/// .map(|kv| kv.coords()); +/// ``` +/// Either just iterate by reference if you cannot consume the hash map using `.into_iter()`: +/// ``` +/// # use zelll::ParticleLike; +/// # use std::collections::HashMap; +/// # let data = [("a", [0.0f64; 3]); 10]; +/// # let points = HashMap::from(data); +/// // this iterator is not consuming the hash map +/// points.iter().map(|kv| kv.coords()); +/// ``` +/// Or replicate `.copied()` behavior manually (preferred method): +/// ``` +/// # use zelll::ParticleLike; +/// # use std::collections::HashMap; +/// # let data = [("a", [0.0f64; 3]); 10]; +/// # let points = HashMap::from(data); +/// // this iterator manually copies its values +/// points.iter() +/// .map(|(&k, &v)| (k, v)) +/// .map(|kv| kv.coords()); +/// ``` +impl ParticleLike<[T; N]> for &P +where + P: ParticleLike<[T; N]>, +{ + #[inline] + fn coords(&self) -> [T; N] { + (*self).coords() + } +} + #[allow(dead_code)] #[cfg(test)] mod tests {