1use super::{Orientation, Point, Position};
7use crate::Transform;
8use hoomd_manifold::{Hyperbolic, HyperbolicDisk, Minkowski};
9use hoomd_vector::Angle;
10use rand::{
11 Rng,
12 distr::{Distribution, Uniform},
13};
14use robust::{Coord, orient2d};
15use std::f64::consts::PI;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
37pub struct OrientedHyperbolicPoint<const N: usize, R> {
38 pub position: Hyperbolic<N>,
40 pub orientation: R,
42}
43
44impl OrientedHyperbolicPoint<3, Angle> {
45 #[must_use]
49 #[inline]
50 pub fn intersection_point(theta: f64, boost: f64) -> (f64, f64) {
51 let rad = (boost.sinh()) / (1.0 + (boost.cosh())); let x_0 = (1.0 + rad.powi(2)
53 - ((1.0 + rad.powi(2)).powi(2) - 4.0 * (rad.powi(2)) * ((theta.cos()).powi(2))).sqrt())
54 / (2.0 * rad * (theta.cos()));
55 let p = Hyperbolic::<3>::from_polar_coordinates(boost, theta);
56 let intersect = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
57 2.0 * x_0 / (1.0 - x_0.powi(2)),
58 0.0,
59 (1.0 + x_0.powi(2)) / (1.0 - x_0.powi(2)),
60 ]));
61 let angle_change = Self::parallel_transport_angle(&p, &intersect);
62 let end_boost = (intersect.coordinates()[2]).acosh();
63 (end_boost, angle_change)
64 }
65 #[must_use]
68 #[inline]
69 pub fn parallel_transport_angle(start: &Hyperbolic<3>, destination: &Hyperbolic<3>) -> f64 {
70 let (b1, theta_1) = (
71 (start.coordinates()[2]).acosh(),
72 (start.coordinates()[1]).atan2(start.coordinates()[0]),
73 );
74 let (b2, theta_2) = (
75 (destination.coordinates()[2]).acosh(),
76 (destination.coordinates()[1]).atan2(destination.coordinates()[0]),
77 );
78 let phi_1 = -(theta_2 - theta_1) / 2.0;
80 let phi_2 = (theta_2 - theta_1) / 2.0;
81
82 let p = [
83 (b1.sinh()) * (phi_1.cos()) / (1.0 + (b1.cosh())),
84 (b1.sinh()) * (phi_1.sin()) / (1.0 + (b1.cosh())),
85 ];
86 let q = [
87 (b2.sinh()) * (phi_2.cos()) / (1.0 + (b2.cosh())),
88 (b2.sinh()) * (phi_2.sin()) / (1.0 + (b2.cosh())),
89 ];
90 let center = [
91 ((p[0].powi(2) + p[1].powi(2) + 1.0) * q[1]
92 - (q[0].powi(2) + q[1].powi(2) + 1.0) * p[1])
93 / (2.0 * (p[0] * q[1] - q[0] * p[1])),
94 (-(p[0].powi(2) + p[1].powi(2) + 1.0) * q[0]
95 + (q[0].powi(2) + q[1].powi(2) + 1.0) * p[0])
96 / (2.0 * (p[0] * q[1] - q[0] * p[1])),
97 ];
98 let theta_1 = ((p[1] - center[1]).atan2(p[0] - center[0])).rem_euclid(2.0 * PI);
99 let theta_2 = ((q[1] - center[1]).atan2(q[0] - center[0])).rem_euclid(2.0 * PI);
100 let abs_delta_theta = ((theta_2 - theta_1).abs()).rem_euclid(2.0 * PI);
101 let p_a = Coord { x: p[0], y: p[1] };
103 let p_b = Coord { x: q[0], y: q[1] };
104 let p_c = Coord {
105 x: center[0],
106 y: center[1],
107 };
108 let sign = orient2d(p_a, p_b, p_c).signum();
109 if sign.is_nan() {
110 0.0
111 } else {
112 sign * abs_delta_theta
113 }
114 }
115 #[inline]
128 #[must_use]
129 pub fn deck_transform(boost: f64, rotation: f64, position: &Hyperbolic<3>) -> f64 {
130 let tau_over_two = boost / 2.0;
131 let poincare = position.to_poincare();
132 -2.0 * (((tau_over_two.sinh())
133 * ((rotation.cos()) * poincare[1] - (rotation.sin()) * poincare[0]))
134 .atan2(
135 (tau_over_two.cosh())
136 + (tau_over_two.sinh())
137 * ((rotation.cos()) * poincare[0] + (rotation.sin()) * poincare[1]),
138 ))
139 }
140 #[inline]
148 #[must_use]
149 pub fn sample<R: Rng + ?Sized>(
150 disk: &HyperbolicDisk,
151 rng: &mut R,
152 ) -> (Hyperbolic<3>, f64, f64) {
153 let max_boost = disk.disk_radius.get();
154 let point = disk.point;
155 let eta = (point.coordinates()[2]).acosh();
156 let phi = point.coordinates()[1].atan2(point.coordinates()[0]);
157 let trial_boost = Uniform::new(0.0, 1.0).expect("r is positive and real");
158 let trial_rotation =
159 Uniform::new(-PI, PI).expect("hard-coded distribution should be valid");
160 let theta = trial_rotation.sample(rng);
161 let v1: f64 = trial_boost.sample(rng);
162 let v = v1.sqrt() * max_boost;
163 let (v_sinh, eta_sinh, eta_cosh) = (v.sinh(), eta.sinh(), eta.cosh());
164 let (phi_sin, phi_cos) = (phi.sin(), phi.cos());
165 let trial_coords = [v_sinh * theta.cos(), v_sinh * theta.sin(), v.cosh()];
166 let transformed_point = Minkowski::from([
167 trial_coords[0] * (eta_cosh * (phi_cos.powi(2)) + phi_sin.powi(2))
168 + trial_coords[1] * phi_sin * phi_cos * (eta_cosh - 1.0)
169 + trial_coords[2] * eta_sinh * phi_cos,
170 trial_coords[0] * phi_sin * phi_cos * (eta_cosh - 1.0)
171 + trial_coords[1] * (eta_cosh * (phi_sin.powi(2)) + phi_cos.powi(2))
172 + trial_coords[2] * eta_sinh * phi_sin,
173 trial_coords[0] * eta_sinh * phi_cos
174 + trial_coords[1] * eta_sinh * phi_sin
175 + trial_coords[2] * eta_cosh,
176 ]);
177 (
178 Hyperbolic::from_minkowski_coordinates(transformed_point),
179 v,
180 theta,
181 )
182 }
183}
184
185impl Transform<Point<Hyperbolic<3>>> for OrientedHyperbolicPoint<3, Angle> {
187 #[inline]
228 fn transform(&self, site_properties: &Point<Hyperbolic<3>>) -> Point<Hyperbolic<3>> {
229 let body_pos = self.position.coordinates();
230 let body_pos_theta = (body_pos[1].atan2(body_pos[0])).rem_euclid(2.0 * PI);
231 let body_pos_boost = body_pos[2].acosh();
232 let body_angle_system = self.orientation.theta;
233 let body_angle_body =
234 Self::deck_transform(-body_pos_boost, body_pos_theta, self.position())
235 + body_angle_system;
236 let site_pos = site_properties.position.coordinates();
237 let (body_pos_boost_cosh, body_pos_boost_sinh) =
238 (body_pos_boost.cosh(), body_pos_boost.sinh());
239 let (bpb_cos, bpb_sin, diff_cos, diff_sin) = (
240 body_pos_theta.cos(),
241 body_pos_theta.sin(),
242 (body_angle_body - body_pos_theta).cos(),
243 (body_angle_body - body_pos_theta).sin(),
244 );
245 let transformed_point = Minkowski::from([
246 site_pos[0]
247 * (body_pos_boost_cosh * bpb_cos * diff_cos - diff_sin * (body_pos_theta).sin())
248 - site_pos[1] * (bpb_sin * diff_cos + body_pos_boost_cosh * bpb_cos * diff_sin)
249 + site_pos[2] * body_pos_boost_sinh * bpb_cos,
250 site_pos[0] * (body_pos_boost_cosh * bpb_sin * diff_cos + bpb_cos * diff_sin)
251 + site_pos[1] * (bpb_cos * diff_cos - body_pos_boost_cosh * bpb_sin * diff_sin)
252 + site_pos[2] * body_pos_boost_sinh * bpb_sin,
253 site_pos[0] * body_pos_boost_sinh * diff_cos
254 - site_pos[1] * body_pos_boost_sinh * diff_sin
255 + site_pos[2] * body_pos_boost_cosh,
256 ]);
257 let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
258 Point::new(new_hyperbolic)
259 }
260}
261
262impl Transform<OrientedHyperbolicPoint<3, Angle>> for OrientedHyperbolicPoint<3, Angle> {
263 #[inline]
264 fn transform(
265 &self,
266 site_properties: &OrientedHyperbolicPoint<3, Angle>,
267 ) -> OrientedHyperbolicPoint<3, Angle> {
268 let body_pos = self.position.coordinates();
269 let body_pos_theta = body_pos[1].atan2(body_pos[0]);
270 let body_pos_boost = body_pos[2].acosh();
271 let body_angle_system = self.orientation.theta;
272 let body_angle_body =
273 Self::deck_transform(-body_pos_boost, body_pos_theta, self.position())
274 + body_angle_system;
275 let (body_pos_boost_cosh, body_pos_boost_sinh) =
276 (body_pos_boost.cosh(), body_pos_boost.sinh());
277 let (bpb_cos, bpb_sin, diff_cos, diff_sin) = (
278 body_pos_theta.cos(),
279 body_pos_theta.sin(),
280 (body_angle_body - body_pos_theta).cos(),
281 (body_angle_body - body_pos_theta).sin(),
282 );
283 let site_pos = site_properties.position.coordinates();
284 let transformed_point = Minkowski::from([
285 site_pos[0] * (body_pos_boost_cosh * bpb_cos * diff_cos - diff_sin * bpb_sin)
286 - site_pos[1] * (bpb_sin * diff_cos + body_pos_boost_cosh * bpb_cos * diff_sin)
287 + site_pos[2] * body_pos_boost_sinh * bpb_cos,
288 site_pos[0] * (body_pos_boost_cosh * bpb_sin * diff_cos + bpb_cos * diff_sin)
289 + site_pos[1] * (bpb_cos * diff_cos - body_pos_boost_cosh * bpb_sin * diff_sin)
290 + site_pos[2] * body_pos_boost_sinh * bpb_sin,
291 site_pos[0] * body_pos_boost_sinh * diff_cos
292 - site_pos[1] * body_pos_boost_sinh * diff_sin
293 + site_pos[2] * body_pos_boost_cosh,
294 ]);
295 let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
296 let site_angle_system =
297 Self::deck_transform(body_pos_boost, body_pos_theta, &site_properties.position)
298 + site_properties.orientation.theta
299 + body_angle_system;
300 OrientedHyperbolicPoint {
301 position: new_hyperbolic,
302 orientation: Angle::from(site_angle_system),
303 }
304 }
305}
306
307impl<const N: usize, R> Position for OrientedHyperbolicPoint<N, R> {
308 type Position = Hyperbolic<N>;
309
310 #[inline]
311 fn position(&self) -> &Hyperbolic<N> {
312 &self.position
313 }
314
315 #[inline]
316 fn position_mut(&mut self) -> &mut Hyperbolic<N> {
317 &mut self.position
318 }
319}
320
321impl<const N: usize, R> Orientation for OrientedHyperbolicPoint<N, R> {
322 type Rotation = R;
323
324 #[inline]
325 fn orientation(&self) -> &R {
326 &self.orientation
327 }
328
329 #[inline]
330 fn orientation_mut(&mut self) -> &mut R {
331 &mut self.orientation
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use approxim::assert_relative_eq;
339 use hoomd_geometry::shape::EightEight;
340 use hoomd_vector::{Angle, Metric};
341 use std::f64::consts::PI;
342
343 #[test]
344 fn transform_oriented_h2_point() {
345 let body_boost = 1.1;
346 let body_orientation = PI / 2.0;
347 let site_boost = 0.1;
348 let body = OrientedHyperbolicPoint {
349 position: Hyperbolic::<3>::from_polar_coordinates(body_boost, 0.0),
350 orientation: Angle::from(body_orientation),
351 };
352 let site = Point::new(Hyperbolic::<3>::from_polar_coordinates(
353 site_boost,
354 -PI / 4.0,
355 ));
356 let transformed_site = body.transform(&site);
357 assert_relative_eq!(
358 *transformed_site.position().point(),
359 [
360 (body_boost.sinh()) * (site_boost.cosh())
361 + ((PI / 4.0).cos()) * (body_boost.cosh()) * (site_boost.sinh()),
362 ((PI / 4.0).sin()) * site_boost.sinh(),
363 (body_boost.cosh()) * (site_boost.cosh())
364 + ((PI / 4.0).cos()) * (body_boost.sinh()) * (site_boost.sinh()),
365 ]
366 .into(),
367 epsilon = 1e-12
368 );
369 }
370
371 #[test]
372 fn parallel_transport_method() {
373 let p = [-(2.0_f64).sqrt() / 2.0, (2.0_f64).sqrt() / 2.0];
374 let q = [(2.0_f64).sqrt() / 2.0, (2.0_f64).sqrt() / 2.0];
375 let center = [
376 ((p[0].powi(2) + p[1].powi(2) + 1.0) * q[1]
377 - (q[0].powi(2) + q[1].powi(2) + 1.0) * p[1])
378 / (2.0 * (p[0] * q[1] - q[0] * p[1])),
379 (-(p[0].powi(2) + p[1].powi(2) + 1.0) * q[0]
380 + (q[0].powi(2) + q[1].powi(2) + 1.0) * p[0])
381 / (2.0 * (p[0] * q[1] - q[0] * p[1])),
382 ];
383 let theta_1 = ((p[1] - center[1]).atan2(p[0] - center[0])).rem_euclid(2.0 * PI);
384 let theta_2 = ((q[1] - center[1]).atan2(q[0] - center[0])).rem_euclid(2.0 * PI);
385 let abs_delta_theta = ((theta_2 - theta_1).abs()).rem_euclid(2.0 * PI);
386 let p_a = Coord { x: p[0], y: p[1] };
388 let p_b = Coord { x: q[0], y: q[1] };
389 let p_c = Coord {
390 x: center[0],
391 y: center[1],
392 };
393 let sign = orient2d(p_a, p_b, p_c).signum();
394 let del_angle = sign * abs_delta_theta;
395 let answer = PI / 2.0;
396
397 assert_relative_eq!(answer, del_angle, epsilon = 1e-8);
398 }
399
400 #[test]
401 fn parallel_transport() {
402 let p1 = Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, -PI / 4.0);
403 let p2 = Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, 0.0);
404 let del_angle = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(&p1, &p2);
405 assert_relative_eq!(-PI / 2.0, del_angle, epsilon = 1e-12);
406 }
407
408 #[test]
409 fn intersection_points() {
410 let (x_0, del_theta) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
411 PI / 8.0,
412 EightEight::EIGHTEIGHT,
413 );
414 assert_relative_eq!(EightEight::CUSP_TO_EDGE, x_0, epsilon = 1e-12);
415 assert_relative_eq!(PI / 4.0, del_theta, epsilon = 1e-12);
416 }
417
418 #[test]
419 fn shortest_distance() {
420 let offset_boost = 0.2;
421 let v: f64 = 2.448_452_447_678_076;
422 let point = Hyperbolic::<3>::from_polar_coordinates(v - offset_boost, 0.01);
423
424 let point_in_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
426 (v.cosh()) * point.coordinates()[0] - (v.sinh()) * point.coordinates()[2],
427 point.coordinates()[1],
428 -(v.sinh()) * point.coordinates()[0] + (v.cosh()) * point.coordinates()[2],
429 ]));
430 let (angle_c, boost_c) = (
431 point_in_center.coordinates()[1].atan2(point_in_center.coordinates()[0]),
432 (point_in_center.coordinates()[2]).acosh(),
433 );
434 let (int_o, _o_o) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
435 angle_c - 7.0 * PI / 8.0,
436 boost_c,
437 );
438 let intersection_o = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
439 (v.cosh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.sinh()) * (int_o.cosh()),
440 (int_o.sinh()) * ((7.0 * PI / 8.0).sin()),
441 (v.sinh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_o.cosh()),
442 ]));
443 let blip = 0.05;
444 let intersection_plus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
445 (v.cosh()) * ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).cos())
446 + (v.sinh()) * ((blip + int_o).cosh()),
447 ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).sin()),
448 (v.sinh()) * ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).cos())
449 + (v.cosh()) * ((blip + int_o).cosh()),
450 ]));
451 let intersection_minus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
452 (v.cosh()) * ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).cos())
453 + (v.sinh()) * ((blip - int_o).cosh()),
454 ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).sin()),
455 (v.sinh()) * ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).cos())
456 + (v.cosh()) * ((blip - int_o).cosh()),
457 ]));
458 assert!(point.distance(&intersection_plus) > point.distance(&intersection_o));
459 assert!(point.distance(&intersection_minus) > point.distance(&intersection_o));
460 }
461}