1use approxim::{approx_derive::RelativeEq, assert_relative_eq};
7use rand::{
8 Rng,
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};
16
17#[derive(Clone, Copy, Debug, PartialEq, RelativeEq, Serialize, Deserialize)]
27pub struct Spherical<const N: usize> {
28 point: Cartesian<N>,
30}
31impl<const N: usize> Spherical<N> {
32 #[inline]
34 #[must_use]
35 pub fn coordinates(&self) -> &[f64; N] {
36 &self.point.coordinates
37 }
38 #[inline]
40 #[must_use]
41 pub fn point(&self) -> &Cartesian<N> {
42 &self.point
43 }
44 #[inline]
50 #[must_use]
51 pub fn from_cartesian_coordinates(point: Cartesian<N>) -> Spherical<N> {
52 let rad = point.norm();
53 assert_relative_eq!(rad, 1.0_f64, epsilon = 1e-6);
54 Spherical { point }
55 }
56
57 #[inline]
76 #[must_use]
77 pub fn stereographic_projection(&self) -> Vec<f64> {
78 (0..N - 1)
79 .collect::<Vec<usize>>()
80 .iter()
81 .map(|i| self.point.coordinates[*i] / (1.0 - self.point.coordinates[N - 1]))
82 .collect::<Vec<f64>>()
83 }
84}
85
86impl Spherical<3> {
87 #[inline]
89 #[must_use]
90 pub fn from_polar_coordinates(theta: f64, phi: f64) -> Spherical<3> {
91 let theta_mod = theta.rem_euclid(PI);
92 let phi_mod = phi.rem_euclid(2.0 * PI);
93 let point = Cartesian::from([
94 (theta_mod.sin()) * (phi_mod.cos()),
95 (theta_mod.sin()) * (phi_mod.sin()),
96 (theta_mod.cos()),
97 ]);
98 Spherical::from_cartesian_coordinates(point)
99 }
100}
101
102impl Spherical<4> {
103 #[inline]
105 #[must_use]
106 pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> {
107 let theta_mod = theta.rem_euclid(PI);
108 let phi_1_mod = phi_1.rem_euclid(PI);
109 let phi_2_mod = phi_2.rem_euclid(2.0 * PI);
110 let point = Cartesian::from([
111 (theta_mod.sin()) * (phi_1_mod.cos()),
112 (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.cos()),
113 (theta_mod.sin()) * (phi_1_mod.sin()) * (phi_2_mod.sin()),
114 theta_mod.cos(),
115 ]);
116 Spherical::from_cartesian_coordinates(point)
117 }
118}
119
120impl Metric for Spherical<3> {
121 #[inline]
132 fn distance(&self, other: &Self) -> f64 {
133 let arg = Cartesian::dot(&self.point, &other.point);
134 arg.acos()
135 }
136 #[inline]
137 fn distance_squared(&self, other: &Self) -> f64 {
138 (self.distance(other)).powi(2)
139 }
140 #[inline]
141 fn n_dimensions(&self) -> usize {
142 2_usize
143 }
144}
145
146impl Metric for Spherical<4> {
147 #[inline]
159 fn distance(&self, other: &Self) -> f64 {
160 let arg = Cartesian::dot(&self.point, &other.point);
161 arg.acos()
162 }
163 #[inline]
164 fn distance_squared(&self, other: &Self) -> f64 {
165 (self.distance(other)).powi(2)
166 }
167 #[inline]
168 fn n_dimensions(&self) -> usize {
169 3_usize
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
209pub struct SphericalDisk {
210 pub disk_radius: PositiveReal,
212 pub point: Spherical<3>,
214}
215
216impl<const N: usize> Default for Spherical<N> {
217 #[inline]
218 fn default() -> Self {
219 let mut zero = Cartesian::<N>::default();
220 zero.coordinates[N - 1] = 1.0;
221 Spherical { point: zero }
222 }
223}
224
225impl Distribution<Spherical<3>> for SphericalDisk {
226 #[inline]
227 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Spherical<3> {
228 let max_angle = self.disk_radius.get();
229 let p = self.point.point.coordinates;
230 let p_norm = self.point.point.norm();
231 let p_hat = [p[0] / p_norm, p[1] / p_norm, p[2] / p_norm];
232 let tmp = if p_hat[2].abs() < 0.9 {
234 [0.0, 0.0, 1.0]
235 } else {
236 [1.0, 0.0, 0.0]
237 };
238
239 let e1 = [
241 tmp[1] * p_hat[2] - tmp[2] * p_hat[1],
242 tmp[2] * p_hat[0] - tmp[0] * p_hat[2],
243 tmp[0] * p_hat[1] - tmp[1] * p_hat[0],
244 ];
245 let e1_norm = (e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]).sqrt();
246 let e1 = [e1[0] / e1_norm, e1[1] / e1_norm, e1[2] / e1_norm];
247 let e2 = [
249 p_hat[1] * e1[2] - p_hat[2] * e1[1],
250 p_hat[2] * e1[0] - p_hat[0] * e1[2],
251 p_hat[0] * e1[1] - p_hat[1] * e1[0],
252 ];
253 let phi = Uniform::new(0.0, 2.0 * PI)
255 .expect("u is positive")
256 .sample(rng);
257 let dir = [
258 phi.cos() * e1[0] + phi.sin() * e2[0],
259 phi.cos() * e1[1] + phi.sin() * e2[1],
260 phi.cos() * e1[2] + phi.sin() * e2[2],
261 ];
262 let u: f64 = Uniform::new(0.0, 1.0).expect("u is positive").sample(rng);
264 let cos_theta = 1.0 - u * (1.0 - max_angle.cos());
265 let sin_theta = (cos_theta.acos()).sin();
266 let new_point = [
268 cos_theta * p_hat[0] + sin_theta * dir[0],
269 cos_theta * p_hat[1] + sin_theta * dir[1],
270 cos_theta * p_hat[2] + sin_theta * dir[2],
271 ];
272
273 Spherical::from_cartesian_coordinates(Cartesian::from(new_point))
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use approxim::assert_relative_eq;
281 use rand::{SeedableRng, rngs::StdRng};
282
283 fn generate_s2_pair() -> (Spherical<3>, Spherical<3>) {
285 (
286 Spherical::<3>::from_polar_coordinates(0.1, 0.3),
287 Spherical::<3>::from_polar_coordinates(1.1, 0.5),
288 )
289 }
290 fn generate_s3_pair() -> (Spherical<4>, Spherical<4>) {
292 (
293 Spherical::<4>::from_polar_coordinates(0.2, 0.3, 0.5),
294 Spherical::<4>::from_polar_coordinates(2.3, 1.1, 0.4),
295 )
296 }
297
298 #[test]
299 fn spherical_distance() {
300 let (a, b) = generate_s2_pair();
301 let ab_distance = a.distance(&b);
302 let ab_distance_numeric = 1.002_106_222_125_083;
303 assert_relative_eq!(ab_distance, ab_distance_numeric, epsilon = 1e-12);
304
305 let (c, d) = generate_s3_pair();
306 let cd_distance = c.distance(&d);
307 let cd_distance_numeric = 2.153_128_900_772_028;
308 assert_relative_eq!(cd_distance, cd_distance_numeric, epsilon = 1e-12);
309 }
310
311 #[test]
312 fn stereographic() {
313 let a = Spherical::<3>::from_polar_coordinates(2.1, 1.5);
314 let a_projection = a.stereographic_projection();
315 let a_projection_numeric = [0.040_576_252_191_799_88, 0.572_182_772_038_917_1];
316 assert_relative_eq![a_projection[0], a_projection_numeric[0], epsilon = 1e-12];
317 assert_relative_eq![a_projection[1], a_projection_numeric[1], epsilon = 1e-12];
318
319 let b = Spherical::<4>::from_polar_coordinates(2.1, 1.5, 0.5);
320 let b_projection = b.stereographic_projection();
321 let b_projection_numeric = [
322 0.040_576_252_191_799_88,
323 0.502_137_622_955_448_1,
324 0.274_319_033_664_803_76,
325 ];
326 assert_relative_eq![b_projection[0], b_projection_numeric[0], epsilon = 1e-12];
327 assert_relative_eq![b_projection[1], b_projection_numeric[1], epsilon = 1e-12];
328 assert_relative_eq![b_projection[2], b_projection_numeric[2], epsilon = 1e-12];
329 }
330
331 #[test]
332 fn random_sphere() {
333 let mut rng = StdRng::seed_from_u64(42);
335 let d = 0.1;
336 let n_pole = Cartesian::from([0.0, 0.0, 1.0]);
337 for _n in 0..10 {
338 let disk = SphericalDisk {
339 disk_radius: d.try_into().expect("hard-coded positive number"),
340 point: Spherical::<3>::from_cartesian_coordinates(n_pole),
341 };
342 let random_point: Spherical<3> = disk.sample(&mut rng);
343
344 let rho = random_point.point.norm_squared();
346 assert_relative_eq!(rho, 1.0, epsilon = 1e-12);
347
348 let distance = (random_point.point[2].acos()) * (rho.sqrt());
350 assert!(d > distance);
351 }
352 }
353}