1use approxim::{approx_derive::RelativeEq, assert_relative_eq};
7use rand::{
8 Rng, RngExt,
9 distr::{Distribution, Uniform},
10};
11use serde::{Deserialize, Serialize};
12use std::f64::consts::PI;
13
14use hoomd_utility::valid::PositiveReal;
15use hoomd_vector::{Cartesian, InnerProduct, Metric, Quaternion, Rotate, Versor};
16
17#[derive(Clone, Copy, Debug, PartialEq, RelativeEq, Serialize, Deserialize)]
31pub struct Spherical<const N: usize> {
32 point: Cartesian<N>,
34}
35impl<const N: usize> Spherical<N> {
36 #[inline]
38 #[must_use]
39 pub fn coordinates(&self) -> &[f64; N] {
40 &self.point.coordinates
41 }
42 #[inline]
44 #[must_use]
45 pub fn point(&self) -> &Cartesian<N> {
46 &self.point
47 }
48 #[inline]
54 #[must_use]
55 pub fn from_cartesian_coordinates(point: Cartesian<N>) -> Spherical<N> {
56 let rad = point.norm();
57 assert_relative_eq!(rad, 1.0_f64, epsilon = 1e-6);
58 Spherical { point }
59 }
60 #[inline]
81 #[must_use]
82 pub fn stereographic_projection(&self) -> Vec<f64> {
83 (0..N - 1)
84 .collect::<Vec<usize>>()
85 .iter()
86 .map(|i| self.point.coordinates[*i] / (1.0 - self.point.coordinates[N - 1]))
87 .collect::<Vec<f64>>()
88 }
89}
90
91impl Spherical<3> {
92 #[inline]
94 #[must_use]
95 pub fn from_polar_coordinates(theta: f64, phi: f64) -> Spherical<3> {
96 let theta_mod = theta.rem_euclid(PI);
97 let phi_mod = phi.rem_euclid(2.0 * PI);
98 let point = Cartesian::from([
99 (theta_mod.sin()) * (phi_mod.cos()),
100 (theta_mod.sin()) * (phi_mod.sin()),
101 (theta_mod.cos()),
102 ]);
103 Spherical::from_cartesian_coordinates(point)
104 }
105}
106
107impl Spherical<4> {
108 #[inline]
121 #[must_use]
122 pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> {
123 let theta_mod = theta.rem_euclid(PI);
124 let phi_1_mod = phi_1.rem_euclid(PI);
125 let phi_2_mod = phi_2.rem_euclid(2.0 * PI);
126 let point = Cartesian::from([
127 (theta_mod.sin()) * (phi_1_mod.cos()),
128 (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.cos()),
129 (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.sin()),
130 theta_mod.cos(),
131 ]);
132 Spherical::from_cartesian_coordinates(point)
133 }
134 #[inline]
136 #[must_use]
137 pub fn from_versor(versor: Versor) -> Spherical<4> {
138 let quat = versor.get();
139 let (a, b, c, d) = (quat.scalar, quat.vector[0], quat.vector[1], quat.vector[2]);
140 Spherical::<4>::from_cartesian_coordinates(Cartesian::from([a, b, c, d]))
141 }
142 #[inline]
191 #[must_use]
192 pub fn to_versor(&self) -> Versor {
193 let phi = self.coordinates()[3].atan2(self.coordinates()[2]);
194 let theta = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2)).sqrt())
195 .atan2(self.coordinates()[1]);
196 let psi = ((self.coordinates()[3].powi(2)
197 + self.coordinates()[2].powi(2)
198 + self.coordinates()[1].powi(2))
199 .sqrt())
200 .atan2(self.coordinates()[0]);
201 let n_hat = Cartesian::from([
202 theta.cos(),
203 (theta.sin()) * (phi.cos()),
204 (theta.sin()) * (phi.sin()),
205 ])
206 .to_unit_unchecked();
207 Versor::from_axis_angle(n_hat.0, psi)
208 }
209}
210
211impl Metric for Spherical<3> {
212 #[inline]
223 fn distance(&self, other: &Self) -> f64 {
224 let arg = Cartesian::dot(&self.point, &other.point);
225 let arg_clipped = arg.clamp(-1.0, 1.0);
226 arg_clipped.acos()
227 }
228 #[inline]
229 fn distance_squared(&self, other: &Self) -> f64 {
230 (self.distance(other)).powi(2)
231 }
232 #[inline]
233 fn n_dimensions(&self) -> usize {
234 2_usize
235 }
236}
237
238impl Metric for Spherical<4> {
239 #[inline]
251 fn distance(&self, other: &Self) -> f64 {
252 let arg = Cartesian::dot(&self.point, &other.point);
253 let arg_clipped = arg.clamp(-1.0, 1.0);
254 arg_clipped.acos()
255 }
256 #[inline]
257 fn distance_squared(&self, other: &Self) -> f64 {
258 (self.distance(other)).powi(2)
259 }
260 #[inline]
261 fn n_dimensions(&self) -> usize {
262 3_usize
263 }
264}
265
266#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302pub struct SphericalDisk<const N: usize> {
303 pub disk_radius: PositiveReal,
305 pub point: Spherical<N>,
307}
308
309impl<const N: usize> Default for Spherical<N> {
310 #[inline]
311 fn default() -> Self {
312 let mut zero = Cartesian::<N>::default();
313 zero.coordinates[N - 1] = 1.0;
314 Spherical { point: zero }
315 }
316}
317
318impl Distribution<Spherical<3>> for SphericalDisk<3> {
319 #[inline]
322 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Spherical<3> {
323 let max_angle = self.disk_radius.get();
324 let p = self.point.point.coordinates;
325 let p_norm = self.point.point.norm();
326 let p_hat = [p[0] / p_norm, p[1] / p_norm, p[2] / p_norm];
327 let tmp = if p_hat[2].abs() < 0.9 {
329 [0.0, 0.0, 1.0]
330 } else {
331 [1.0, 0.0, 0.0]
332 };
333
334 let e1 = [
336 tmp[1] * p_hat[2] - tmp[2] * p_hat[1],
337 tmp[2] * p_hat[0] - tmp[0] * p_hat[2],
338 tmp[0] * p_hat[1] - tmp[1] * p_hat[0],
339 ];
340 let e1_norm = (e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]).sqrt();
341 let e1 = [e1[0] / e1_norm, e1[1] / e1_norm, e1[2] / e1_norm];
342 let e2 = [
344 p_hat[1] * e1[2] - p_hat[2] * e1[1],
345 p_hat[2] * e1[0] - p_hat[0] * e1[2],
346 p_hat[0] * e1[1] - p_hat[1] * e1[0],
347 ];
348 let phi = Uniform::new(0.0, 2.0 * PI)
350 .expect("u is positive")
351 .sample(rng);
352 let dir = [
353 phi.cos() * e1[0] + phi.sin() * e2[0],
354 phi.cos() * e1[1] + phi.sin() * e2[1],
355 phi.cos() * e1[2] + phi.sin() * e2[2],
356 ];
357 let u: f64 = Uniform::new(0.0, 1.0).expect("u is positive").sample(rng);
359 let cos_theta = 1.0 - u * (1.0 - max_angle.cos());
360 let sin_theta = (cos_theta.acos()).sin();
361 let new_point = [
363 cos_theta * p_hat[0] + sin_theta * dir[0],
364 cos_theta * p_hat[1] + sin_theta * dir[1],
365 cos_theta * p_hat[2] + sin_theta * dir[2],
366 ];
367
368 Spherical::from_cartesian_coordinates(Cartesian::from(new_point))
369 }
370}
371
372impl Distribution<Spherical<4>> for SphericalDisk<4> {
373 #[inline]
376 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Spherical<4> {
377 let max_trans = self.disk_radius.get();
378 let point = self.point;
379 let v: Versor = rng.random();
381 let b_hat = v
382 .rotate(&Cartesian::from([1.0, 0.0, 0.0]))
383 .to_unit()
384 .expect("hard coded non-null vector");
385 let eta = Uniform::new(0.0, max_trans).expect("hard coded non-negative");
386 let translation_versor = Versor::from_axis_angle(b_hat.0, eta.sample(rng));
387
388 let position_versor = Quaternion::from(*point.coordinates())
389 .to_versor()
390 .expect("spherical points cannot be null");
391 let transformation =
392 ((*translation_versor.get()) * (*position_versor.get()) * (*translation_versor.get()))
393 .to_versor()
394 .expect("spherical points cannot be null");
395 let sphere_point = Spherical::<4>::from_versor(transformation);
396 Spherical::<4>::from_cartesian_coordinates(*sphere_point.point())
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use approxim::assert_relative_eq;
404 use rand::{SeedableRng, rngs::StdRng};
405
406 fn generate_s2_pair() -> (Spherical<3>, Spherical<3>) {
408 (
409 Spherical::<3>::from_polar_coordinates(0.1, 0.3),
410 Spherical::<3>::from_polar_coordinates(1.1, 0.5),
411 )
412 }
413 fn generate_s3_pair() -> (Spherical<4>, Spherical<4>) {
415 (
416 Spherical::<4>::from_polar_coordinates(0.2, 0.3, 0.5),
417 Spherical::<4>::from_polar_coordinates(2.3, 1.1, 0.4),
418 )
419 }
420
421 #[test]
422 fn spherical_distance() {
423 let (a, b) = generate_s2_pair();
424 let ab_distance = a.distance(&b);
425 let ab_distance_numeric = 1.002_106_222_125_083;
426 assert_relative_eq!(ab_distance, ab_distance_numeric, epsilon = 1e-12);
427
428 let (c, d) = generate_s3_pair();
429 let cd_distance = c.distance(&d);
430 let cd_distance_numeric = 2.153_128_900_772_028;
431 assert_relative_eq!(cd_distance, cd_distance_numeric, epsilon = 1e-12);
432 }
433
434 #[test]
435 fn stereographic() {
436 let a = Spherical::<3>::from_polar_coordinates(2.1, 1.5);
437 let a_projection = a.stereographic_projection();
438 let a_projection_numeric = [0.040_576_252_191_799_88, 0.572_182_772_038_917_1];
439 assert_relative_eq![a_projection[0], a_projection_numeric[0], epsilon = 1e-12];
440 assert_relative_eq![a_projection[1], a_projection_numeric[1], epsilon = 1e-12];
441
442 let b = Spherical::<4>::from_polar_coordinates(2.1, 1.5, 0.5);
443 let b_projection = b.stereographic_projection();
444 let b_projection_numeric = [
445 0.040_576_252_191_799_88,
446 0.502_137_622_955_448_1,
447 0.274_319_033_664_803_76,
448 ];
449 assert_relative_eq![b_projection[0], b_projection_numeric[0], epsilon = 1e-12];
450 assert_relative_eq![b_projection[1], b_projection_numeric[1], epsilon = 1e-12];
451 assert_relative_eq![b_projection[2], b_projection_numeric[2], epsilon = 1e-12];
452 }
453
454 #[test]
455 fn random_two_sphere() {
456 let mut rng = StdRng::seed_from_u64(42);
458 let d = 0.1;
459 let n_pole = Cartesian::from([0.0, 0.0, 1.0]);
460 for _n in 0..10 {
461 let disk = SphericalDisk {
462 disk_radius: d.try_into().expect("hard-coded positive number"),
463 point: Spherical::<3>::from_cartesian_coordinates(n_pole),
464 };
465 let random_point: Spherical<3> = disk.sample(&mut rng);
466
467 let rho = random_point.point.norm_squared();
469 assert_relative_eq!(rho, 1.0, epsilon = 1e-12);
470
471 let distance = (random_point.point[2].acos()) * (rho.sqrt());
473 assert!(d > distance);
474 }
475 }
476 #[test]
477 fn random_three_sphere() {
478 let mut rng = StdRng::seed_from_u64(42);
480 let d = 0.1;
481 let n_pole = Spherical::from_cartesian_coordinates(Cartesian::from([1.0, 0.0, 0.0, 0.0]));
482 for _n in 0..10 {
483 let disk = SphericalDisk {
484 disk_radius: d.try_into().expect("hard-coded positive number"),
485 point: n_pole,
486 };
487 let random_point: Spherical<4> = disk.sample(&mut rng);
488
489 let rho = random_point.point.norm_squared();
491 assert_relative_eq!(rho, 1.0, epsilon = 1e-12);
492
493 let distance = random_point.point().distance(n_pole.point());
495 assert!(d > distance);
496 }
497 }
498 #[test]
499 fn from_versor() {
500 let mut rng = StdRng::seed_from_u64(358);
502 let v: Versor = rng.random();
503 let sphere_pt = Spherical::<4>::from_versor(v);
504 assert_eq!(sphere_pt.coordinates()[0], v.get().scalar);
505 assert_eq!(sphere_pt.coordinates()[1], v.get().vector.coordinates[0]);
506 assert_eq!(sphere_pt.coordinates()[2], v.get().vector.coordinates[1]);
507 assert_eq!(sphere_pt.coordinates()[3], v.get().vector.coordinates[2]);
508 }
509 #[test]
510 fn to_versor() {
511 let mut rng = StdRng::seed_from_u64(5121);
513 let v: Versor = rng.random();
514 let sphere_pt = Spherical::<4>::from_versor(v);
515 let versor_from_spherical = sphere_pt.to_versor();
517 let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0])
518 .to_versor()
519 .expect("not a null vector");
520 let transformation =
521 (*versor_from_spherical.get() * *pole_versor.get() * *versor_from_spherical.get())
522 .to_versor()
523 .expect("Hard-coded example is valid");
524 let q = transformation.get();
525 assert_relative_eq!(q.scalar, v.get().scalar, epsilon = 1e-12);
526 assert_relative_eq!(
527 q.vector.coordinates[0],
528 v.get().vector.coordinates[0],
529 epsilon = 1e-12
530 );
531 assert_relative_eq!(
532 q.vector.coordinates[1],
533 v.get().vector.coordinates[1],
534 epsilon = 1e-12
535 );
536 assert_relative_eq!(
537 q.vector.coordinates[2],
538 v.get().vector.coordinates[2],
539 epsilon = 1e-12
540 );
541 }
542}