55
66use approxim:: { approx_derive:: RelativeEq , assert_relative_eq} ;
77use rand:: {
8- Rng ,
8+ Rng , RngExt ,
99 distr:: { Distribution , Uniform } ,
1010} ;
1111use serde:: { Deserialize , Serialize } ;
1212use std:: f64:: consts:: PI ;
1313
1414use hoomd_utility:: valid:: PositiveReal ;
15- use hoomd_vector:: { Cartesian , InnerProduct , Metric } ;
15+ use hoomd_vector:: { Cartesian , InnerProduct , Metric , Quaternion , Rotate , Versor } ;
1616
1717/// Point on the surface of a sphere.
1818///
@@ -53,8 +53,8 @@ impl<const N: usize> Spherical<N> {
5353 assert_relative_eq ! ( rad, 1.0_f64 , epsilon = 1e-6 ) ;
5454 Spherical { point }
5555 }
56-
57- /// Implements a stereographic projection from the N-sphere to an N-dimensional plane .
56+ /// Implements a stereographic projection from the N-sphere to an n-dimensional
57+ /// plane by projecting through the $`(0,\cdots, 0,1)`$ axis .
5858 ///
5959 /// # Example
6060 /// ```
@@ -100,7 +100,18 @@ impl Spherical<3> {
100100}
101101
102102impl Spherical < 4 > {
103- /// Create a 3-sphere from spherical coordinates
103+ /// Create a point on a 3-sphere from spherical coordinates. Note that this uses
104+ /// the convention
105+ /// ```math
106+ /// \begin{pmatrix}
107+ /// \sin\theta \cos\phi_1
108+ /// \\ \sin\theta \sin\phi_1 \cos\phi_2
109+ /// \\ \sin\theta \sin\phi_1 \sin\phi_2
110+ /// \\ \cos\theta
111+ /// \end{pmatrix}
112+ /// ```
113+ /// where $`\theta`$ and $`phi_1`$ both run over the range $`0`$ to $`\pi`$ and $`\phi_2`$
114+ /// runs from $`0`$ to $`2\pi`$.
104115 #[ inline]
105116 #[ must_use]
106117 pub fn from_polar_coordinates ( theta : f64 , phi_1 : f64 , phi_2 : f64 ) -> Spherical < 4 > {
@@ -115,6 +126,80 @@ impl Spherical<4> {
115126 ] ) ;
116127 Spherical :: from_cartesian_coordinates ( point)
117128 }
129+ /// Create a point on a unit-radius 3-sphere from a unit quaternion.
130+ #[ inline]
131+ #[ must_use]
132+ pub fn from_versor ( versor : Versor ) -> Spherical < 4 > {
133+ let quat = versor. get ( ) ;
134+ let ( a, b, c, d) = ( quat. scalar , quat. vector [ 0 ] , quat. vector [ 1 ] , quat. vector [ 2 ] ) ;
135+ Spherical :: < 4 > :: from_cartesian_coordinates ( Cartesian :: from ( [ a, b, c, d] ) )
136+ }
137+ /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point.
138+ /// # Example
139+ /// ```
140+ /// use approxim::assert_relative_eq;
141+ /// use hoomd_manifold::Spherical;
142+ /// use hoomd_vector::{Cartesian, Quaternion, Versor};
143+ /// use std::f64::consts::PI;
144+ ///
145+ /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
146+ /// let radius = 1.0;
147+ /// let x = Spherical::<4>::from_polar_coordinates(
148+ /// PI / 4.0,
149+ /// PI / 8.0,
150+ /// 5.0 * PI / 4.0,
151+ /// );
152+ /// let x_versor = x.to_versor();
153+ /// let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0])
154+ /// .to_versor()
155+ /// .expect("not a null vector");
156+ /// let transformation =
157+ /// (*x_versor.get() * *pole_versor.get() * *x_versor.get())
158+ /// .to_versor()
159+ /// .expect("Hard-coded example is valid");
160+ /// let mapped_pole = Spherical::<4>::from_versor(transformation);
161+ /// assert_relative_eq!(
162+ /// mapped_pole.coordinates()[0],
163+ /// x.coordinates()[0],
164+ /// epsilon = 1e-12
165+ /// );
166+ /// assert_relative_eq!(
167+ /// mapped_pole.coordinates()[1],
168+ /// x.coordinates()[1],
169+ /// epsilon = 1e-12
170+ /// );
171+ /// assert_relative_eq!(
172+ /// mapped_pole.coordinates()[2],
173+ /// x.coordinates()[2],
174+ /// epsilon = 1e-12
175+ /// );
176+ /// assert_relative_eq!(
177+ /// mapped_pole.coordinates()[3],
178+ /// x.coordinates()[3],
179+ /// epsilon = 1e-12
180+ /// );
181+ /// # Ok(())
182+ /// # }
183+ /// ```
184+ #[ inline]
185+ #[ must_use]
186+ pub fn to_versor ( & self ) -> Versor {
187+ let phi = self . coordinates ( ) [ 3 ] . atan2 ( self . coordinates ( ) [ 2 ] ) ;
188+ let theta = ( ( self . coordinates ( ) [ 3 ] . powi ( 2 ) + self . coordinates ( ) [ 2 ] . powi ( 2 ) ) . sqrt ( ) )
189+ . atan2 ( self . coordinates ( ) [ 1 ] ) ;
190+ let psi = ( ( self . coordinates ( ) [ 3 ] . powi ( 2 )
191+ + self . coordinates ( ) [ 2 ] . powi ( 2 )
192+ + self . coordinates ( ) [ 1 ] . powi ( 2 ) )
193+ . sqrt ( ) )
194+ . atan2 ( self . coordinates ( ) [ 0 ] ) ;
195+ let n_hat = Cartesian :: from ( [
196+ theta. cos ( ) ,
197+ ( theta. sin ( ) ) * ( phi. cos ( ) ) ,
198+ ( theta. sin ( ) ) * ( phi. sin ( ) ) ,
199+ ] )
200+ . to_unit_unchecked ( ) ;
201+ Versor :: from_axis_angle ( n_hat. 0 , psi)
202+ }
118203}
119204
120205impl Metric for Spherical < 3 > {
@@ -131,7 +216,8 @@ impl Metric for Spherical<3> {
131216 #[ inline]
132217 fn distance ( & self , other : & Self ) -> f64 {
133218 let arg = Cartesian :: dot ( & self . point , & other. point ) ;
134- arg. acos ( )
219+ let arg_clipped = arg. clamp ( -1.0 , 1.0 ) ;
220+ arg_clipped. acos ( )
135221 }
136222 #[ inline]
137223 fn distance_squared ( & self , other : & Self ) -> f64 {
@@ -158,7 +244,8 @@ impl Metric for Spherical<4> {
158244 #[ inline]
159245 fn distance ( & self , other : & Self ) -> f64 {
160246 let arg = Cartesian :: dot ( & self . point , & other. point ) ;
161- arg. acos ( )
247+ let arg_clipped = arg. clamp ( -1.0 , 1.0 ) ;
248+ arg_clipped. acos ( )
162249 }
163250 #[ inline]
164251 fn distance_squared ( & self , other : & Self ) -> f64 {
@@ -206,11 +293,11 @@ impl Metric for Spherical<4> {
206293/// # }
207294/// ```
208295#[ derive( Clone , Debug , PartialEq , Serialize , Deserialize ) ]
209- pub struct SphericalDisk {
296+ pub struct SphericalDisk < const N : usize > {
210297 /// Max distance away from point.
211298 pub disk_radius : PositiveReal ,
212299 /// The center of the disk.
213- pub point : Spherical < 3 > ,
300+ pub point : Spherical < N > ,
214301}
215302
216303impl < const N : usize > Default for Spherical < N > {
@@ -222,7 +309,9 @@ impl<const N: usize> Default for Spherical<N> {
222309 }
223310}
224311
225- impl Distribution < Spherical < 3 > > for SphericalDisk {
312+ impl Distribution < Spherical < 3 > > for SphericalDisk < 3 > {
313+ /// Translates 3-dimensional cartesian vector named "point" along the
314+ /// surface of a sphere by maximum distance of r.
226315 #[ inline]
227316 fn sample < R : Rng + ?Sized > ( & self , rng : & mut R ) -> Spherical < 3 > {
228317 let max_angle = self . disk_radius . get ( ) ;
@@ -274,6 +363,34 @@ impl Distribution<Spherical<3>> for SphericalDisk {
274363 }
275364}
276365
366+ impl Distribution < Spherical < 4 > > for SphericalDisk < 4 > {
367+ /// Translates 3-dimensional cartesian vector named "point" along the
368+ /// surface of a sphere by maximum distance of r.
369+ #[ inline]
370+ fn sample < R : Rng + ?Sized > ( & self , rng : & mut R ) -> Spherical < 4 > {
371+ let max_trans = self . disk_radius . get ( ) ;
372+ let point = self . point ;
373+ // generate random unit cartesian vector
374+ let v: Versor = rng. random ( ) ;
375+ let b_hat = v
376+ . rotate ( & Cartesian :: from ( [ 1.0 , 0.0 , 0.0 ] ) )
377+ . to_unit ( )
378+ . expect ( "hard coded non-null vector" ) ;
379+ let eta = Uniform :: new ( 0.0 , max_trans) . expect ( "hard coded non-negative" ) ;
380+ let translation_versor = Versor :: from_axis_angle ( b_hat. 0 , eta. sample ( rng) ) ;
381+
382+ let position_versor = Quaternion :: from ( * point. coordinates ( ) )
383+ . to_versor ( )
384+ . expect ( "spherical points cannot be null" ) ;
385+ let transformation =
386+ ( ( * translation_versor. get ( ) ) * ( * position_versor. get ( ) ) * ( * translation_versor. get ( ) ) )
387+ . to_versor ( )
388+ . expect ( "spherical points cannot be null" ) ;
389+ let sphere_point = Spherical :: < 4 > :: from_versor ( transformation) ;
390+ Spherical :: < 4 > :: from_cartesian_coordinates ( * sphere_point. point ( ) )
391+ }
392+ }
393+
277394#[ cfg( test) ]
278395mod tests {
279396 use super :: * ;
@@ -329,7 +446,7 @@ mod tests {
329446 }
330447
331448 #[ test]
332- fn random_sphere ( ) {
449+ fn random_two_sphere ( ) {
333450 // Generate ten random points on the Hyperbolic
334451 let mut rng = StdRng :: seed_from_u64 ( 42 ) ;
335452 let d = 0.1 ;
@@ -350,4 +467,70 @@ mod tests {
350467 assert ! ( d > distance) ;
351468 }
352469 }
470+ #[ test]
471+ fn random_three_sphere ( ) {
472+ // Generate ten random points on the Hyperbolic
473+ let mut rng = StdRng :: seed_from_u64 ( 42 ) ;
474+ let d = 0.1 ;
475+ let n_pole = Spherical :: from_cartesian_coordinates ( Cartesian :: from ( [ 1.0 , 0.0 , 0.0 , 0.0 ] ) ) ;
476+ for _n in 0 ..10 {
477+ let disk = SphericalDisk {
478+ disk_radius : d. try_into ( ) . expect ( "hard-coded positive number" ) ,
479+ point : n_pole,
480+ } ;
481+ let random_point: Spherical < 4 > = disk. sample ( & mut rng) ;
482+
483+ // check that points remain on Sphere
484+ let rho = random_point. point . norm_squared ( ) ;
485+ assert_relative_eq ! ( rho, 1.0 , epsilon = 1e-12 ) ;
486+
487+ // check that points are within distance d of north pole
488+ let distance = random_point. point ( ) . distance ( n_pole. point ( ) ) ;
489+ assert ! ( d > distance) ;
490+ }
491+ }
492+ #[ test]
493+ fn from_versor ( ) {
494+ // generate a 3-sphere point from a versor
495+ let mut rng = StdRng :: seed_from_u64 ( 358 ) ;
496+ let v: Versor = rng. random ( ) ;
497+ let sphere_pt = Spherical :: < 4 > :: from_versor ( v) ;
498+ assert_eq ! ( sphere_pt. coordinates( ) [ 0 ] , v. get( ) . scalar) ;
499+ assert_eq ! ( sphere_pt. coordinates( ) [ 1 ] , v. get( ) . vector. coordinates[ 0 ] ) ;
500+ assert_eq ! ( sphere_pt. coordinates( ) [ 2 ] , v. get( ) . vector. coordinates[ 1 ] ) ;
501+ assert_eq ! ( sphere_pt. coordinates( ) [ 3 ] , v. get( ) . vector. coordinates[ 2 ] ) ;
502+ }
503+ #[ test]
504+ fn to_versor ( ) {
505+ // generate a 3-sphere point from a versor
506+ let mut rng = StdRng :: seed_from_u64 ( 5121 ) ;
507+ let v: Versor = rng. random ( ) ;
508+ let sphere_pt = Spherical :: < 4 > :: from_versor ( v) ;
509+ // get versor which maps identity versor to 3-sphere point
510+ let versor_from_spherical = sphere_pt. to_versor ( ) ;
511+ let pole_versor = Quaternion :: from ( [ 1.0 , 0.0 , 0.0 , 0.0 ] )
512+ . to_versor ( )
513+ . expect ( "not a null vector" ) ;
514+ let transformation =
515+ ( * versor_from_spherical. get ( ) * * pole_versor. get ( ) * * versor_from_spherical. get ( ) )
516+ . to_versor ( )
517+ . expect ( "Hard-coded example is valid" ) ;
518+ let q = transformation. get ( ) ;
519+ assert_relative_eq ! ( q. scalar, v. get( ) . scalar, epsilon = 1e-12 ) ;
520+ assert_relative_eq ! (
521+ q. vector. coordinates[ 0 ] ,
522+ v. get( ) . vector. coordinates[ 0 ] ,
523+ epsilon = 1e-12
524+ ) ;
525+ assert_relative_eq ! (
526+ q. vector. coordinates[ 1 ] ,
527+ v. get( ) . vector. coordinates[ 1 ] ,
528+ epsilon = 1e-12
529+ ) ;
530+ assert_relative_eq ! (
531+ q. vector. coordinates[ 2 ] ,
532+ v. get( ) . vector. coordinates[ 2 ] ,
533+ epsilon = 1e-12
534+ ) ;
535+ }
353536}
0 commit comments