From 43f6930aa37357af9431c43f21f0fb89d6e69dc5 Mon Sep 17 00:00:00 2001 From: mthran Date: Mon, 16 Feb 2026 16:03:18 -0500 Subject: [PATCH 01/15] Reduce precision on radius check --- hoomd-mc/src/translate/sphere.rs | 62 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 9c44dfa7e..4cfe64fd9 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -64,4 +64,64 @@ impl LocalTrial>> for Translate>> { } } -// TODO: test +impl LocalTrial>> for Translate>> { + /// Propose local trial moves for a body on the surface of a 3-sphere + /// + /// # Example + /// ``` + /// use approxim::assert_relative_eq; + /// use hoomd_manifold::{Spherical, SphericalDisk}; + /// use hoomd_mc::{LocalTrial, Translate}; + /// use hoomd_microstate::property::{Point, Position}; + /// use hoomd_vector::{Cartesian, Metric, Vector}; + /// use rand::{Rng, SeedableRng, rngs::StdRng}; + /// use std::f64::consts::PI; + /// + /// # fn main() -> Result<(), Box> { + /// let mut rng = StdRng::seed_from_u64(14); + /// let radius: f64 = 1.0; + /// let initial_point = Point::new( + /// Spherical::<4>::from_polar_coordinates(1.0, PI/4.0, PI/10.0, 5.0*PI/4.0) + /// ); + /// let d = 0.1; + /// let translate = Translate::with_maximum_distance(d.try_into()?); + /// + /// let new_body_properties = translate.propose(&mut rng, initial_point); + /// + /// // Translation move keeps point on the surface of the sphere + /// assert_eq!(new_body_properties.position().radius(), radius,); + /// + /// // Translation move does not translate the point more than a distance d away + /// assert!( + /// d > new_body_properties + /// .position() + /// .distance(&initial_point.position()) + /// ); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { + let mut trial = body_properties; + let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); + let (sn, cs) = (displacement.sin(), displacement.cos()); + let vec = Cartesian::<4>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); + let proj = vec.dot(trial.position.point()); + let tangent = Cartesian::from([ + vec[0] - proj * trial.position.coordinates()[0], + vec[1] - proj * trial.position.coordinates()[1], + vec[2] - proj * trial.position.coordinates()[2], + vec[3] - proj * trial.position.coordinates()[3], + ]); + let (unit, _norm) = tangent.to_unit().expect("cannot be null"); + let new = Cartesian::from([ + trial.position.coordinates()[0] * cs + unit.get().coordinates[0]*sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1]*sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2]*sn, + trial.position.coordinates()[3] * cs + unit.get().coordinates[3]*sn, + ]); + *trial.position_mut() = Spherical::from_cartesian_coordinates( + new); + trial + } +} From a9fd62441057ef25c873e1e39f17ad6f566c89b9 Mon Sep 17 00:00:00 2001 From: mthran Date: Mon, 16 Feb 2026 14:43:18 -0500 Subject: [PATCH 02/15] Change trial moves on sphere to exponential map --- Cargo.lock | 7 +++---- hoomd-mc/src/translate/sphere.rs | 36 ++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40fc1a19b..62cfb9b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3749,10 +3749,9 @@ dependencies = [ "hoomd-spatial", "hoomd-utility", "hoomd-vector", - "itertools", - "log", - "rand 0.10.1", - "rand_distr 0.6.0", + "itertools 0.14.0", + "rand 0.9.2", + "rand_distr 0.5.1", "rayon", "rstest", "serde", diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 4cfe64fd9..eebf4f2a4 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -3,12 +3,13 @@ //! Implement Translation moves on curved surfaces -use rand::{Rng, distr::Distribution}; +use rand::Rng; use crate::{LocalTrial, Translate}; -use hoomd_manifold::{Spherical, SphericalDisk}; +use hoomd_manifold::Spherical; use hoomd_microstate::property::{Point, Position}; -use hoomd_vector::InnerProduct; +use hoomd_vector::{Cartesian, InnerProduct}; +use rand_distr::StandardNormal; impl LocalTrial>> for Translate>> { /// Propose local trial moves for a body on the surface of a sphere @@ -24,8 +25,10 @@ impl LocalTrial>> for Translate>> { /// /// # fn main() -> Result<(), Box> { /// let mut rng = StdRng::seed_from_u64(14); + /// let radius: f64 = 1.0; /// let initial_point = Point::new(Spherical::from_cartesian_coordinates( /// [0.5_f64.sqrt(), 0.5_f64.sqrt(), 0.0].into(), + /// 1.0_f64, /// )); /// let d = 0.1; /// let translate = Translate::with_maximum_distance(d.try_into()?); @@ -52,14 +55,25 @@ impl LocalTrial>> for Translate>> { body_properties: Point>, ) -> Point> { let mut trial = body_properties; - let disk = SphericalDisk { - disk_radius: *self.maximum_distance(), - point: *trial.position_mut(), - }; - *trial.position_mut() = disk.sample(rng); - let rescale = 1.0 / trial.position().point().norm(); - *trial.position_mut() = - Spherical::from_cartesian_coordinates(*trial.position().point() * rescale); + let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); + let (sn, cs) = (displacement.sin(), displacement.cos()); + let vec = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); + let proj = vec.dot(trial.position.point()); + println!("proj: {:}", proj); + let tangent = Cartesian::from([ + vec[0] - proj * trial.position.coordinates()[0], + vec[1] - proj * trial.position.coordinates()[1], + vec[2] - proj * trial.position.coordinates()[2], + ]); + let (unit, _norm) = tangent.to_unit().expect("cannot be null"); + println!("new unit vector: {:?}", unit); + let new = Cartesian::from([ + trial.position.coordinates()[0] * cs + unit.get().coordinates[0]*sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1]*sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2]*sn, + ]); + *trial.position_mut() = Spherical::from_cartesian_coordinates( + new); trial } } From a567199859adcf92b0eb94dc8ef8422e4a3d13e7 Mon Sep 17 00:00:00 2001 From: mthran Date: Thu, 18 Dec 2025 13:13:31 -0500 Subject: [PATCH 03/15] Update sphere.rs --- Cargo.lock | 7 +++-- hoomd-manifold/src/sphere.rs | 48 ++++++++++++++++++++++++++++++-- hoomd-mc/src/translate/sphere.rs | 4 +-- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62cfb9b92..40fc1a19b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3749,9 +3749,10 @@ dependencies = [ "hoomd-spatial", "hoomd-utility", "hoomd-vector", - "itertools 0.14.0", - "rand 0.9.2", - "rand_distr 0.5.1", + "itertools", + "log", + "rand 0.10.1", + "rand_distr 0.6.0", "rayon", "rstest", "serde", diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index 9145b7ced..da51bc730 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use std::f64::consts::PI; use hoomd_utility::valid::PositiveReal; -use hoomd_vector::{Cartesian, InnerProduct, Metric}; +use hoomd_vector::{Cartesian, InnerProduct, Metric, Quaternion, Rotate, Versor}; /// Point on the surface of a sphere. /// @@ -100,7 +100,7 @@ impl Spherical<3> { } impl Spherical<4> { - /// Create a 3-sphere from spherical coordinates + /// Create a point on a 3-sphere from spherical coordinates #[inline] #[must_use] pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> { @@ -115,6 +115,50 @@ impl Spherical<4> { ]); Spherical::from_cartesian_coordinates(point) } + /// Create a point on a 3-sphere from a unit quaternion. + #[inline] + #[must_use] + pub fn from_versor(versor: Versor) -> Spherical<4> { + let a = versor.get().scalar; + let [b,c,d] = versor.get().vector.coordinates; + Spherical::<4>::from_cartesian_coordinates( + Cartesian::from([a,b,c,d])) + } + /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point. + /// # Example + /// ``` + /// use hoomd_manifold::Spherical; + /// use hoomd_vector::{Cartesian, Quaternion, Versor}; + /// use approxim::assert_relative_eq; + /// use std::f64::consts::PI; + /// + /// # fn main() -> Result<(), Box> { + /// let radius = 1.0; + /// let x = Spherical::<4>::from_polar_coordinates(1.0,PI/4.0, PI/8.0, 5.0*PI/4.0); + + /// let x_versor = x.to_versor(); + /// let pole_versor = Quaternion::from([1.0,0.0,0.0,0.0]).to_versor().expect("not a null vector"); + /// let transformation = (*x_versor.get() * *pole_versor.get() * *x_versor.get()) + /// .to_versor() + /// .expect("Hard-coded example is valid"); + /// let mapped_pole = Spherical::<4>::from_versor(transformation); + /// + /// assert_relative_eq!(mapped_pole.coordinates()[0], x.coordinates()[0], epsilon=1e-12); + /// assert_relative_eq!(mapped_pole.coordinates()[1], x.coordinates()[1], epsilon=1e-12); + /// assert_relative_eq!(mapped_pole.coordinates()[2], x.coordinates()[2], epsilon=1e-12); + /// assert_relative_eq!(mapped_pole.coordinates()[3], x.coordinates()[3], epsilon=1e-12); + /// # Ok(()) + /// # } + /// ``` + #[inline] + #[must_use] + pub fn to_versor(&self) -> Versor { + let phi = self.coordinates()[3].atan2(self.coordinates()[2]); + let theta = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2)).sqrt()).atan2(self.coordinates()[1]); + let psi = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2) + self.coordinates()[1].powi(2)).sqrt()).atan2(self.coordinates()[0]); + let n_hat = Cartesian::from([theta.cos(), (theta.sin())*(phi.cos()), (theta.sin())*(phi.sin())]).to_unit_unchecked(); + Versor::from_axis_angle(n_hat.0, psi) + } } impl Metric for Spherical<3> { diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index eebf4f2a4..042617a31 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -3,13 +3,13 @@ //! Implement Translation moves on curved surfaces -use rand::Rng; +use rand::{Rng, RngExt}; +use rand_distr::StandardNormal; use crate::{LocalTrial, Translate}; use hoomd_manifold::Spherical; use hoomd_microstate::property::{Point, Position}; use hoomd_vector::{Cartesian, InnerProduct}; -use rand_distr::StandardNormal; impl LocalTrial>> for Translate>> { /// Propose local trial moves for a body on the surface of a sphere From 82a1fdf792f93136db6659a7b11e115e00e5e108 Mon Sep 17 00:00:00 2001 From: mthran Date: Wed, 17 Dec 2025 17:13:55 -0500 Subject: [PATCH 04/15] trial moves for 3-sphere --- hoomd-manifold/src/sphere.rs | 54 ++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index da51bc730..5cac6de56 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -5,7 +5,7 @@ use approxim::{approx_derive::RelativeEq, assert_relative_eq}; use rand::{ - Rng, + Rng, RngExt, distr::{Distribution, Uniform}, }; use serde::{Deserialize, Serialize}; @@ -250,11 +250,11 @@ impl Metric for Spherical<4> { /// # } /// ``` #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SphericalDisk { +pub struct SphericalDisk { /// Max distance away from point. pub disk_radius: PositiveReal, /// The center of the disk. - pub point: Spherical<3>, + pub point: Spherical, } impl Default for Spherical { @@ -266,7 +266,9 @@ impl Default for Spherical { } } -impl Distribution> for SphericalDisk { +impl Distribution> for SphericalDisk<3> { + /// Translates 3-dimensional cartesian vector named "point" along the + /// surface of a sphere by maximum distance of r. #[inline] fn sample(&self, rng: &mut R) -> Spherical<3> { let max_angle = self.disk_radius.get(); @@ -318,6 +320,26 @@ impl Distribution> for SphericalDisk { } } +impl Distribution> for SphericalDisk<4> { + /// Translates 3-dimensional cartesian vector named "point" along the + /// surface of a sphere by maximum distance of r. + #[inline] + fn sample(&self, rng: &mut R) -> Spherical<4> { + let max_trans = self.disk_radius.get(); + let point = self.point; + // generate random unit cartesian vector + let v : Versor = rng.random(); + let b_hat = v.rotate(&Cartesian::from([1.0,0.0,0.0])).to_unit().expect("hard coded non-null vector"); + let eta = Uniform::new(0.0, max_trans).expect("hard coded non-negative"); + let translation_versor = Versor::from_axis_angle(b_hat.0, eta.sample(rng)); + + let position_versor = Quaternion::from(*point.coordinates()).to_versor().expect("spherical points cannot be null"); + let transformation = ((*translation_versor.get()) * (*position_versor.get()) * (*translation_versor.get())).to_versor().expect("spherical points cannot be null"); + let sphere_point = Spherical::<4>::from_versor(transformation); + Spherical::<4>::from_cartesian_coordinates(*sphere_point.point()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -373,7 +395,7 @@ mod tests { } #[test] - fn random_sphere() { + fn random_two_sphere() { // Generate ten random points on the Hyperbolic let mut rng = StdRng::seed_from_u64(42); let d = 0.1; @@ -394,4 +416,26 @@ mod tests { assert!(d > distance); } } + #[test] + fn random_three_sphere() { + // Generate ten random points on the Hyperbolic + let mut rng = StdRng::seed_from_u64(42); + let d = 0.1; + let n_pole = Spherical::from_cartesian_coordinates(Cartesian::from([1.0, 0.0, 0.0, 0.0])); + for _n in 0..10 { + let disk = SphericalDisk { + disk_radius: d.try_into().expect("hard-coded positive number"), + point: n_pole, + }; + let random_point: Spherical<4> = disk.sample(&mut rng); + + // check that points remain on Sphere + let rho = random_point.point.norm_squared(); + assert_relative_eq!(rho, 1.0, epsilon = 1e-12); + + // check that points are within distance d of north pole + let distance = random_point.point().distance(n_pole.point()); + assert!(d > distance); + } + } } From 4479072b3b79431249f3a04282e3b3daea9186ac Mon Sep 17 00:00:00 2001 From: mthran Date: Wed, 17 Dec 2025 16:08:35 -0500 Subject: [PATCH 05/15] translate for 3-sphere --- hoomd-manifold/src/sphere.rs | 37 +++++++++++++++----------- hoomd-mc/src/translate/sphere.rs | 6 ++--- hoomd-microstate/src/property/point.rs | 2 +- hoomd-vector/src/quaternion.rs | 9 +++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index 5cac6de56..fd91f8af3 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -53,8 +53,8 @@ impl Spherical { assert_relative_eq!(rad, 1.0_f64, epsilon = 1e-6); Spherical { point } } - - /// Implements a stereographic projection from the N-sphere to an N-dimensional plane. + /// Implements a stereographic projection from the N-sphere to an n-dimensional + /// plane by projecting through the $`(0,\cdots, 0,1)`$ axis. /// /// # Example /// ``` @@ -100,27 +100,36 @@ impl Spherical<3> { } impl Spherical<4> { - /// Create a point on a 3-sphere from spherical coordinates + /// Create a point on a 3-sphere from spherical coordinates. Note that this uses + /// the convention + /// ```math + /// \begin{pmatrix}r\cos\psi + /// \\ r\sin\psi\cos\theta + /// \\ r\sin\psi\sin\theta\cos\phi + /// \\ r\sin\psi\sin\theta\sin\phi + /// \end{pmatrix} + /// ``` + /// where $`\psi`$ and $`theta`$ both run over the range $`0`$ to $`\pi`$ and $`\phi`$ + /// runs from $`0`$ to $`2\pi`$. #[inline] #[must_use] - pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> { + pub fn from_polar_coordinates(psi: f64, theta: f64, phi: f64) -> Spherical<4> { + let psi_mod = psi.rem_euclid(PI); let theta_mod = theta.rem_euclid(PI); - let phi_1_mod = phi_1.rem_euclid(PI); - let phi_2_mod = phi_2.rem_euclid(2.0 * PI); + let phi_mod = phi.rem_euclid(2.0 * PI); let point = Cartesian::from([ - (theta_mod.sin()) * (phi_1_mod.cos()), - (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.cos()), - (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.sin()), - theta_mod.cos(), + (psi_mod.cos()), + (psi_mod.sin()) * (theta_mod.cos()), + (psi_mod.sin()) * (theta_mod.sin()) * (phi_mod.cos()), + (psi_mod.sin()) * (theta_mod.sin()) * (phi_mod.sin()), ]); Spherical::from_cartesian_coordinates(point) } - /// Create a point on a 3-sphere from a unit quaternion. + /// Create a point on a unit-radius 3-sphere from a unit quaternion. #[inline] #[must_use] pub fn from_versor(versor: Versor) -> Spherical<4> { - let a = versor.get().scalar; - let [b,c,d] = versor.get().vector.coordinates; + let (a,b,c,d) = versor.get_components(); Spherical::<4>::from_cartesian_coordinates( Cartesian::from([a,b,c,d])) } @@ -135,14 +144,12 @@ impl Spherical<4> { /// # fn main() -> Result<(), Box> { /// let radius = 1.0; /// let x = Spherical::<4>::from_polar_coordinates(1.0,PI/4.0, PI/8.0, 5.0*PI/4.0); - /// let x_versor = x.to_versor(); /// let pole_versor = Quaternion::from([1.0,0.0,0.0,0.0]).to_versor().expect("not a null vector"); /// let transformation = (*x_versor.get() * *pole_versor.get() * *x_versor.get()) /// .to_versor() /// .expect("Hard-coded example is valid"); /// let mapped_pole = Spherical::<4>::from_versor(transformation); - /// /// assert_relative_eq!(mapped_pole.coordinates()[0], x.coordinates()[0], epsilon=1e-12); /// assert_relative_eq!(mapped_pole.coordinates()[1], x.coordinates()[1], epsilon=1e-12); /// assert_relative_eq!(mapped_pole.coordinates()[2], x.coordinates()[2], epsilon=1e-12); diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 042617a31..ef257875f 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -7,9 +7,9 @@ use rand::{Rng, RngExt}; use rand_distr::StandardNormal; use crate::{LocalTrial, Translate}; -use hoomd_manifold::Spherical; +use hoomd_manifold::{Spherical, SphericalDisk}; use hoomd_microstate::property::{Point, Position}; -use hoomd_vector::{Cartesian, InnerProduct}; +use hoomd_vector::{InnerProduct, Quaternion, Versor}; impl LocalTrial>> for Translate>> { /// Propose local trial moves for a body on the surface of a sphere @@ -115,7 +115,7 @@ impl LocalTrial>> for Translate>> { /// # } /// ``` #[inline] - fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { + fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { let mut trial = body_properties; let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); let (sn, cs) = (displacement.sin(), displacement.cos()); diff --git a/hoomd-microstate/src/property/point.rs b/hoomd-microstate/src/property/point.rs index df19713c4..133856f20 100644 --- a/hoomd-microstate/src/property/point.rs +++ b/hoomd-microstate/src/property/point.rs @@ -222,7 +222,7 @@ impl Transform>> for Point> { /// Move `Point>` properties from the local body frame to the /// system frame. /// - /// All positions on the 3-sphere are associated with some $`SO(4)`$ + /// All positions on the 3-sphere are associated with some $`SU(2)`$ /// transformation which translates the origin to that position. The local /// body frame is the frame in which the body position is the origin. The /// position of the sites in the system frame is obtained by applying the diff --git a/hoomd-vector/src/quaternion.rs b/hoomd-vector/src/quaternion.rs index ed0ecfd4e..e8f4f8a52 100644 --- a/hoomd-vector/src/quaternion.rs +++ b/hoomd-vector/src/quaternion.rs @@ -542,6 +542,15 @@ impl Versor { }) } + /// Get the components of a [`Versor`]. + #[inline] + #[must_use] + pub fn get_components(&self) -> (f64, f64, f64, f64) { + let quaternion = self.get(); + let vec_components = quaternion.vector; + (quaternion.scalar, vec_components[0], vec_components[1], vec_components[2]) + } + /// Normalize the versor. /// /// Nominally, all [`Versor`] instances retain a unit norm. Due to limited From f640748f33385a6e9d7e9f71aedb1cfda7088490 Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 15:35:53 -0400 Subject: [PATCH 06/15] fix documentation --- hoomd-manifold/src/sphere.rs | 27 ++++++++++++++------------- hoomd-mc/src/translate/sphere.rs | 19 ++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index fd91f8af3..01dc22b24 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -103,25 +103,26 @@ impl Spherical<4> { /// Create a point on a 3-sphere from spherical coordinates. Note that this uses /// the convention /// ```math - /// \begin{pmatrix}r\cos\psi - /// \\ r\sin\psi\cos\theta - /// \\ r\sin\psi\sin\theta\cos\phi - /// \\ r\sin\psi\sin\theta\sin\phi + /// \begin{pmatrix} + /// \sin\theta \cos\phi_1 + /// \\ \sin\theta \sin\phi_1 \cos\phi_2 + /// \\ \sin\theta \sin\phi_1 \sin\phi_2 + /// \\ \cos\theta /// \end{pmatrix} /// ``` - /// where $`\psi`$ and $`theta`$ both run over the range $`0`$ to $`\pi`$ and $`\phi`$ + /// where $`\theta`$ and $`phi_1`$ both run over the range $`0`$ to $`\pi`$ and $`\phi_2`$ /// runs from $`0`$ to $`2\pi`$. #[inline] #[must_use] - pub fn from_polar_coordinates(psi: f64, theta: f64, phi: f64) -> Spherical<4> { - let psi_mod = psi.rem_euclid(PI); + pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> { let theta_mod = theta.rem_euclid(PI); - let phi_mod = phi.rem_euclid(2.0 * PI); + let phi_1_mod = phi_1.rem_euclid(PI); + let phi_2_mod = phi_2.rem_euclid(2.0 * PI); let point = Cartesian::from([ - (psi_mod.cos()), - (psi_mod.sin()) * (theta_mod.cos()), - (psi_mod.sin()) * (theta_mod.sin()) * (phi_mod.cos()), - (psi_mod.sin()) * (theta_mod.sin()) * (phi_mod.sin()), + (theta_mod.sin()) * (phi_1_mod.cos()), + (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.cos()), + (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.sin()), + theta_mod.cos(), ]); Spherical::from_cartesian_coordinates(point) } @@ -143,7 +144,7 @@ impl Spherical<4> { /// /// # fn main() -> Result<(), Box> { /// let radius = 1.0; - /// let x = Spherical::<4>::from_polar_coordinates(1.0,PI/4.0, PI/8.0, 5.0*PI/4.0); + /// let x = Spherical::<4>::from_polar_coordinates(PI/4.0, PI/8.0, 5.0*PI/4.0); /// let x_versor = x.to_versor(); /// let pole_versor = Quaternion::from([1.0,0.0,0.0,0.0]).to_versor().expect("not a null vector"); /// let transformation = (*x_versor.get() * *pole_versor.get() * *x_versor.get()) diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index ef257875f..8f45f6702 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -7,9 +7,9 @@ use rand::{Rng, RngExt}; use rand_distr::StandardNormal; use crate::{LocalTrial, Translate}; -use hoomd_manifold::{Spherical, SphericalDisk}; +use hoomd_manifold::Spherical; use hoomd_microstate::property::{Point, Position}; -use hoomd_vector::{InnerProduct, Quaternion, Versor}; +use hoomd_vector::{Cartesian, InnerProduct}; impl LocalTrial>> for Translate>> { /// Propose local trial moves for a body on the surface of a sphere @@ -25,10 +25,8 @@ impl LocalTrial>> for Translate>> { /// /// # fn main() -> Result<(), Box> { /// let mut rng = StdRng::seed_from_u64(14); - /// let radius: f64 = 1.0; /// let initial_point = Point::new(Spherical::from_cartesian_coordinates( /// [0.5_f64.sqrt(), 0.5_f64.sqrt(), 0.0].into(), - /// 1.0_f64, /// )); /// let d = 0.1; /// let translate = Translate::with_maximum_distance(d.try_into()?); @@ -87,15 +85,14 @@ impl LocalTrial>> for Translate>> { /// use hoomd_manifold::{Spherical, SphericalDisk}; /// use hoomd_mc::{LocalTrial, Translate}; /// use hoomd_microstate::property::{Point, Position}; - /// use hoomd_vector::{Cartesian, Metric, Vector}; + /// use hoomd_vector::{Cartesian, InnerProduct, Metric, Vector}; /// use rand::{Rng, SeedableRng, rngs::StdRng}; /// use std::f64::consts::PI; /// /// # fn main() -> Result<(), Box> { /// let mut rng = StdRng::seed_from_u64(14); - /// let radius: f64 = 1.0; /// let initial_point = Point::new( - /// Spherical::<4>::from_polar_coordinates(1.0, PI/4.0, PI/10.0, 5.0*PI/4.0) + /// Spherical::<4>::from_polar_coordinates(PI/4.0, PI/10.0, 5.0*PI/4.0) /// ); /// let d = 0.1; /// let translate = Translate::with_maximum_distance(d.try_into()?); @@ -103,7 +100,11 @@ impl LocalTrial>> for Translate>> { /// let new_body_properties = translate.propose(&mut rng, initial_point); /// /// // Translation move keeps point on the surface of the sphere - /// assert_eq!(new_body_properties.position().radius(), radius,); + /// assert_relative_eq!( + /// new_body_properties.position().point().norm(), + /// 1.0_f64, + /// epsilon = 1e-8 + /// ); /// /// // Translation move does not translate the point more than a distance d away /// assert!( @@ -115,7 +116,7 @@ impl LocalTrial>> for Translate>> { /// # } /// ``` #[inline] - fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { + fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { let mut trial = body_properties; let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); let (sn, cs) = (displacement.sin(), displacement.cos()); From 22d4d5a9dbcf466205abaf32c1c150360b1f34e9 Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 15:38:09 -0400 Subject: [PATCH 07/15] Fix code formatting --- hoomd-manifold/src/sphere.rs | 78 ++++++++++++++++++++++++-------- hoomd-mc/src/translate/sphere.rs | 38 +++++++++------- hoomd-vector/src/quaternion.rs | 7 ++- 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index 01dc22b24..cbff777d8 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -130,31 +130,53 @@ impl Spherical<4> { #[inline] #[must_use] pub fn from_versor(versor: Versor) -> Spherical<4> { - let (a,b,c,d) = versor.get_components(); - Spherical::<4>::from_cartesian_coordinates( - Cartesian::from([a,b,c,d])) + let (a, b, c, d) = versor.get_components(); + Spherical::<4>::from_cartesian_coordinates(Cartesian::from([a, b, c, d])) } /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point. /// # Example /// ``` + /// use approxim::assert_relative_eq; /// use hoomd_manifold::Spherical; /// use hoomd_vector::{Cartesian, Quaternion, Versor}; - /// use approxim::assert_relative_eq; /// use std::f64::consts::PI; /// /// # fn main() -> Result<(), Box> { /// let radius = 1.0; - /// let x = Spherical::<4>::from_polar_coordinates(PI/4.0, PI/8.0, 5.0*PI/4.0); + /// let x = Spherical::<4>::from_polar_coordinates( + /// PI / 4.0, + /// PI / 8.0, + /// 5.0 * PI / 4.0, + /// ); /// let x_versor = x.to_versor(); - /// let pole_versor = Quaternion::from([1.0,0.0,0.0,0.0]).to_versor().expect("not a null vector"); - /// let transformation = (*x_versor.get() * *pole_versor.get() * *x_versor.get()) + /// let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0]) /// .to_versor() - /// .expect("Hard-coded example is valid"); + /// .expect("not a null vector"); + /// let transformation = + /// (*x_versor.get() * *pole_versor.get() * *x_versor.get()) + /// .to_versor() + /// .expect("Hard-coded example is valid"); /// let mapped_pole = Spherical::<4>::from_versor(transformation); - /// assert_relative_eq!(mapped_pole.coordinates()[0], x.coordinates()[0], epsilon=1e-12); - /// assert_relative_eq!(mapped_pole.coordinates()[1], x.coordinates()[1], epsilon=1e-12); - /// assert_relative_eq!(mapped_pole.coordinates()[2], x.coordinates()[2], epsilon=1e-12); - /// assert_relative_eq!(mapped_pole.coordinates()[3], x.coordinates()[3], epsilon=1e-12); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[0], + /// x.coordinates()[0], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[1], + /// x.coordinates()[1], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[2], + /// x.coordinates()[2], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[3], + /// x.coordinates()[3], + /// epsilon = 1e-12 + /// ); /// # Ok(()) /// # } /// ``` @@ -162,9 +184,19 @@ impl Spherical<4> { #[must_use] pub fn to_versor(&self) -> Versor { let phi = self.coordinates()[3].atan2(self.coordinates()[2]); - let theta = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2)).sqrt()).atan2(self.coordinates()[1]); - let psi = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2) + self.coordinates()[1].powi(2)).sqrt()).atan2(self.coordinates()[0]); - let n_hat = Cartesian::from([theta.cos(), (theta.sin())*(phi.cos()), (theta.sin())*(phi.sin())]).to_unit_unchecked(); + let theta = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2)).sqrt()) + .atan2(self.coordinates()[1]); + let psi = ((self.coordinates()[3].powi(2) + + self.coordinates()[2].powi(2) + + self.coordinates()[1].powi(2)) + .sqrt()) + .atan2(self.coordinates()[0]); + let n_hat = Cartesian::from([ + theta.cos(), + (theta.sin()) * (phi.cos()), + (theta.sin()) * (phi.sin()), + ]) + .to_unit_unchecked(); Versor::from_axis_angle(n_hat.0, psi) } } @@ -336,13 +368,21 @@ impl Distribution> for SphericalDisk<4> { let max_trans = self.disk_radius.get(); let point = self.point; // generate random unit cartesian vector - let v : Versor = rng.random(); - let b_hat = v.rotate(&Cartesian::from([1.0,0.0,0.0])).to_unit().expect("hard coded non-null vector"); + let v: Versor = rng.random(); + let b_hat = v + .rotate(&Cartesian::from([1.0, 0.0, 0.0])) + .to_unit() + .expect("hard coded non-null vector"); let eta = Uniform::new(0.0, max_trans).expect("hard coded non-negative"); let translation_versor = Versor::from_axis_angle(b_hat.0, eta.sample(rng)); - let position_versor = Quaternion::from(*point.coordinates()).to_versor().expect("spherical points cannot be null"); - let transformation = ((*translation_versor.get()) * (*position_versor.get()) * (*translation_versor.get())).to_versor().expect("spherical points cannot be null"); + let position_versor = Quaternion::from(*point.coordinates()) + .to_versor() + .expect("spherical points cannot be null"); + let transformation = + ((*translation_versor.get()) * (*position_versor.get()) * (*translation_versor.get())) + .to_versor() + .expect("spherical points cannot be null"); let sphere_point = Spherical::<4>::from_versor(transformation); Spherical::<4>::from_cartesian_coordinates(*sphere_point.point()) } diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 8f45f6702..1ae750be7 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -53,7 +53,7 @@ impl LocalTrial>> for Translate>> { body_properties: Point>, ) -> Point> { let mut trial = body_properties; - let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); + let displacement = (self.maximum_distance().get()) * rng.sample::(StandardNormal); let (sn, cs) = (displacement.sin(), displacement.cos()); let vec = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); let proj = vec.dot(trial.position.point()); @@ -66,12 +66,11 @@ impl LocalTrial>> for Translate>> { let (unit, _norm) = tangent.to_unit().expect("cannot be null"); println!("new unit vector: {:?}", unit); let new = Cartesian::from([ - trial.position.coordinates()[0] * cs + unit.get().coordinates[0]*sn, - trial.position.coordinates()[1] * cs + unit.get().coordinates[1]*sn, - trial.position.coordinates()[2] * cs + unit.get().coordinates[2]*sn, + trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn, ]); - *trial.position_mut() = Spherical::from_cartesian_coordinates( - new); + *trial.position_mut() = Spherical::from_cartesian_coordinates(new); trial } } @@ -91,9 +90,11 @@ impl LocalTrial>> for Translate>> { /// /// # fn main() -> Result<(), Box> { /// let mut rng = StdRng::seed_from_u64(14); - /// let initial_point = Point::new( - /// Spherical::<4>::from_polar_coordinates(PI/4.0, PI/10.0, 5.0*PI/4.0) - /// ); + /// let initial_point = Point::new(Spherical::<4>::from_polar_coordinates( + /// PI / 4.0, + /// PI / 10.0, + /// 5.0 * PI / 4.0, + /// )); /// let d = 0.1; /// let translate = Translate::with_maximum_distance(d.try_into()?); /// @@ -116,9 +117,13 @@ impl LocalTrial>> for Translate>> { /// # } /// ``` #[inline] - fn propose(&self, rng: &mut R, body_properties: Point>) -> Point> { + fn propose( + &self, + rng: &mut R, + body_properties: Point>, + ) -> Point> { let mut trial = body_properties; - let displacement = (self.maximum_distance().get())*rng.sample::(StandardNormal); + let displacement = (self.maximum_distance().get()) * rng.sample::(StandardNormal); let (sn, cs) = (displacement.sin(), displacement.cos()); let vec = Cartesian::<4>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); let proj = vec.dot(trial.position.point()); @@ -130,13 +135,12 @@ impl LocalTrial>> for Translate>> { ]); let (unit, _norm) = tangent.to_unit().expect("cannot be null"); let new = Cartesian::from([ - trial.position.coordinates()[0] * cs + unit.get().coordinates[0]*sn, - trial.position.coordinates()[1] * cs + unit.get().coordinates[1]*sn, - trial.position.coordinates()[2] * cs + unit.get().coordinates[2]*sn, - trial.position.coordinates()[3] * cs + unit.get().coordinates[3]*sn, + trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn, + trial.position.coordinates()[3] * cs + unit.get().coordinates[3] * sn, ]); - *trial.position_mut() = Spherical::from_cartesian_coordinates( - new); + *trial.position_mut() = Spherical::from_cartesian_coordinates(new); trial } } diff --git a/hoomd-vector/src/quaternion.rs b/hoomd-vector/src/quaternion.rs index e8f4f8a52..94094a019 100644 --- a/hoomd-vector/src/quaternion.rs +++ b/hoomd-vector/src/quaternion.rs @@ -548,7 +548,12 @@ impl Versor { pub fn get_components(&self) -> (f64, f64, f64, f64) { let quaternion = self.get(); let vec_components = quaternion.vector; - (quaternion.scalar, vec_components[0], vec_components[1], vec_components[2]) + ( + quaternion.scalar, + vec_components[0], + vec_components[1], + vec_components[2], + ) } /// Normalize the versor. From a8e67249a387a284dfb92686c0eae61e25bc58a4 Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 15:43:12 -0400 Subject: [PATCH 08/15] remove println! --- hoomd-mc/src/translate/sphere.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 1ae750be7..d6ff3b0f6 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -57,14 +57,12 @@ impl LocalTrial>> for Translate>> { let (sn, cs) = (displacement.sin(), displacement.cos()); let vec = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); let proj = vec.dot(trial.position.point()); - println!("proj: {:}", proj); let tangent = Cartesian::from([ vec[0] - proj * trial.position.coordinates()[0], vec[1] - proj * trial.position.coordinates()[1], vec[2] - proj * trial.position.coordinates()[2], ]); let (unit, _norm) = tangent.to_unit().expect("cannot be null"); - println!("new unit vector: {:?}", unit); let new = Cartesian::from([ trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn, trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn, From 4ee1ad2b944ed725a8c66fdfe18af9f35f65b90a Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 15:48:18 -0400 Subject: [PATCH 09/15] clip argument of distance function for spherical points --- hoomd-manifold/src/sphere.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index cbff777d8..131e004a4 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -215,7 +215,8 @@ impl Metric for Spherical<3> { #[inline] fn distance(&self, other: &Self) -> f64 { let arg = Cartesian::dot(&self.point, &other.point); - arg.acos() + let arg_clipped = arg.clamp(-1.0,1.0); + arg_clipped.acos() } #[inline] fn distance_squared(&self, other: &Self) -> f64 { @@ -242,7 +243,8 @@ impl Metric for Spherical<4> { #[inline] fn distance(&self, other: &Self) -> f64 { let arg = Cartesian::dot(&self.point, &other.point); - arg.acos() + let arg_clipped = arg.clamp(-1.0,1.0); + arg_clipped.acos() } #[inline] fn distance_squared(&self, other: &Self) -> f64 { From dae454e0e01899ffc295affa34983fac2bbd3d03 Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 16:04:46 -0400 Subject: [PATCH 10/15] test function for from_versor --- hoomd-manifold/src/sphere.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index 131e004a4..f75456db1 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -488,4 +488,15 @@ mod tests { assert!(d > distance); } } + #[test] + fn from_versor() { + // generate a 3-sphere point from a versor + let mut rng = StdRng::seed_from_u64(112358); + let v: Versor = rng.random(); + let sphere_pt = Spherical::<4>::from_versor(v); + assert_eq!(sphere_pt.coordinates()[0], v.get().scalar); + assert_eq!(sphere_pt.coordinates()[1], v.get().vector.coordinates[0]); + assert_eq!(sphere_pt.coordinates()[2], v.get().vector.coordinates[1]); + assert_eq!(sphere_pt.coordinates()[3], v.get().vector.coordinates[2]); + } } From 3db5d4a7f53b017857d002df79dc34a873a0ad2d Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Mon, 4 May 2026 19:38:16 -0400 Subject: [PATCH 11/15] clippy --- hoomd-manifold/src/sphere.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index f75456db1..e6d10bd20 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -215,7 +215,7 @@ impl Metric for Spherical<3> { #[inline] fn distance(&self, other: &Self) -> f64 { let arg = Cartesian::dot(&self.point, &other.point); - let arg_clipped = arg.clamp(-1.0,1.0); + let arg_clipped = arg.clamp(-1.0, 1.0); arg_clipped.acos() } #[inline] @@ -243,7 +243,7 @@ impl Metric for Spherical<4> { #[inline] fn distance(&self, other: &Self) -> f64 { let arg = Cartesian::dot(&self.point, &other.point); - let arg_clipped = arg.clamp(-1.0,1.0); + let arg_clipped = arg.clamp(-1.0, 1.0); arg_clipped.acos() } #[inline] @@ -491,7 +491,7 @@ mod tests { #[test] fn from_versor() { // generate a 3-sphere point from a versor - let mut rng = StdRng::seed_from_u64(112358); + let mut rng = StdRng::seed_from_u64(358); let v: Versor = rng.random(); let sphere_pt = Spherical::<4>::from_versor(v); assert_eq!(sphere_pt.coordinates()[0], v.get().scalar); From 0a3f1c73203012042da6846b7f7b6ef611ea31d4 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Fri, 8 May 2026 09:51:22 -0400 Subject: [PATCH 12/15] Note Spherical improvements. --- doc/src/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/src/release-notes.md b/doc/src/release-notes.md index d456d6d90..3723cef29 100644 --- a/doc/src/release-notes.md +++ b/doc/src/release-notes.md @@ -4,16 +4,22 @@ *Added:* +* `[hoomd-manifold`: Add `Spherical<4>::from_versor` and the corresponding `::to_versor` (#285). +* `[hoomd-mc`]: Implement translation moves for `Point>` (#287). * `[hoomd-utility]`: Implement `Eq`, `PartialOrd`, and `Ord` for `PositiveReal` (#287). *Changed:* +* `[hoomd-mc`]: Improve the numerical stability of translation moves for `Point>` (#287). + *Deprecated:* *Removed:* *Fixed:* +* `[hoomd-manifold]`: Fixed numerical stability issue in `Spherical<3>::distance` where the dot product could result in an out of bounds value. + ## 1.1.0 (2026-04-17) *Highlights:* From d20984affc1bc59a977637668e45774ef467187e Mon Sep 17 00:00:00 2001 From: mthran Date: Tue, 12 May 2026 10:26:26 -0400 Subject: [PATCH 13/15] PR review suggestions --- hoomd-manifold/src/sphere.rs | 23 ++++++++++++++++++++++- hoomd-vector/src/quaternion.rs | 14 -------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index e6d10bd20..faa269372 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -130,7 +130,8 @@ impl Spherical<4> { #[inline] #[must_use] pub fn from_versor(versor: Versor) -> Spherical<4> { - let (a, b, c, d) = versor.get_components(); + let q = versor.get(); + let (a,b,c,d) = (q.scalar, q.vector[0], q.vector[1], q.vector[2]); Spherical::<4>::from_cartesian_coordinates(Cartesian::from([a, b, c, d])) } /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point. @@ -499,4 +500,24 @@ mod tests { assert_eq!(sphere_pt.coordinates()[2], v.get().vector.coordinates[1]); assert_eq!(sphere_pt.coordinates()[3], v.get().vector.coordinates[2]); } + #[test] + fn to_versor() { + // generate a 3-sphere point from a versor + let mut rng = StdRng::seed_from_u64(5121); + let v: Versor = rng.random(); + let sphere_pt = Spherical::<4>::from_versor(v); + // get versor which maps identity versor to 3-sphere point + let versor_from_spherical = sphere_pt.to_versor(); + let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0]) + .to_versor() + .expect("not a null vector"); + let transformation = (*versor_from_spherical.get() * *pole_versor.get() * *versor_from_spherical.get()) + .to_versor() + .expect("Hard-coded example is valid"); + let q = transformation.get(); + assert_relative_eq!(q.scalar, v.get().scalar,epsilon=1e-12); + assert_relative_eq!(q.vector.coordinates[0], v.get().vector.coordinates[0],epsilon=1e-12); + assert_relative_eq!(q.vector.coordinates[1], v.get().vector.coordinates[1],epsilon=1e-12); + assert_relative_eq!(q.vector.coordinates[2], v.get().vector.coordinates[2],epsilon=1e-12); + } } diff --git a/hoomd-vector/src/quaternion.rs b/hoomd-vector/src/quaternion.rs index 94094a019..ed0ecfd4e 100644 --- a/hoomd-vector/src/quaternion.rs +++ b/hoomd-vector/src/quaternion.rs @@ -542,20 +542,6 @@ impl Versor { }) } - /// Get the components of a [`Versor`]. - #[inline] - #[must_use] - pub fn get_components(&self) -> (f64, f64, f64, f64) { - let quaternion = self.get(); - let vec_components = quaternion.vector; - ( - quaternion.scalar, - vec_components[0], - vec_components[1], - vec_components[2], - ) - } - /// Normalize the versor. /// /// Nominally, all [`Versor`] instances retain a unit norm. Due to limited From d55b56763ef70c16c51cbabe9b1e8c996fdea7cf Mon Sep 17 00:00:00 2001 From: Michelle Thran Date: Tue, 12 May 2026 10:35:01 -0400 Subject: [PATCH 14/15] clippy --- hoomd-manifold/src/sphere.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index faa269372..522376e4c 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -130,8 +130,8 @@ impl Spherical<4> { #[inline] #[must_use] pub fn from_versor(versor: Versor) -> Spherical<4> { - let q = versor.get(); - let (a,b,c,d) = (q.scalar, q.vector[0], q.vector[1], q.vector[2]); + let quat = versor.get(); + let (a, b, c, d) = (quat.scalar, quat.vector[0], quat.vector[1], quat.vector[2]); Spherical::<4>::from_cartesian_coordinates(Cartesian::from([a, b, c, d])) } /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point. @@ -511,13 +511,26 @@ mod tests { let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0]) .to_versor() .expect("not a null vector"); - let transformation = (*versor_from_spherical.get() * *pole_versor.get() * *versor_from_spherical.get()) - .to_versor() - .expect("Hard-coded example is valid"); + let transformation = + (*versor_from_spherical.get() * *pole_versor.get() * *versor_from_spherical.get()) + .to_versor() + .expect("Hard-coded example is valid"); let q = transformation.get(); - assert_relative_eq!(q.scalar, v.get().scalar,epsilon=1e-12); - assert_relative_eq!(q.vector.coordinates[0], v.get().vector.coordinates[0],epsilon=1e-12); - assert_relative_eq!(q.vector.coordinates[1], v.get().vector.coordinates[1],epsilon=1e-12); - assert_relative_eq!(q.vector.coordinates[2], v.get().vector.coordinates[2],epsilon=1e-12); + assert_relative_eq!(q.scalar, v.get().scalar, epsilon = 1e-12); + assert_relative_eq!( + q.vector.coordinates[0], + v.get().vector.coordinates[0], + epsilon = 1e-12 + ); + assert_relative_eq!( + q.vector.coordinates[1], + v.get().vector.coordinates[1], + epsilon = 1e-12 + ); + assert_relative_eq!( + q.vector.coordinates[2], + v.get().vector.coordinates[2], + epsilon = 1e-12 + ); } } From c4432931005eda699ccc13095f1b3ed2adebea54 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Tue, 12 May 2026 16:37:46 -0400 Subject: [PATCH 15/15] fix formatting --- doc/src/release-notes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/src/release-notes.md b/doc/src/release-notes.md index 3723cef29..7886cf270 100644 --- a/doc/src/release-notes.md +++ b/doc/src/release-notes.md @@ -4,13 +4,13 @@ *Added:* -* `[hoomd-manifold`: Add `Spherical<4>::from_versor` and the corresponding `::to_versor` (#285). -* `[hoomd-mc`]: Implement translation moves for `Point>` (#287). +* `[hoomd-manifold]`: Add `Spherical<4>::from_versor` and the corresponding `::to_versor` (#285). +* `[hoomd-mc]`: Implement translation moves for `Point>` (#287). * `[hoomd-utility]`: Implement `Eq`, `PartialOrd`, and `Ord` for `PositiveReal` (#287). *Changed:* -* `[hoomd-mc`]: Improve the numerical stability of translation moves for `Point>` (#287). +* `[hoomd-mc]`: Improve the numerical stability of translation moves for `Point>` (#287). *Deprecated:*