Skip to main content

hoomd_microstate/boundary/periodic/
eighteight.rs

1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement periodic boundary conditions for the {8,8} tiling of hyperbolic
5//! space.
6//!
7//! Specifically, `Periodic<EightEight>` identifies opposite edges of the
8//! octagon to implement the Bolza surface.
9
10use arrayvec::ArrayVec;
11use std::f64::consts::PI;
12
13use crate::{
14    boundary::{
15        Error, GenerateGhosts, MAX_GHOSTS, MaximumAllowableInteractionRange, Periodic, Wrap,
16    },
17    property::{Orientation, OrientedHyperbolicPoint, Point, Position},
18};
19use hoomd_geometry::shape::EightEight;
20use hoomd_manifold::{Hyperbolic, Minkowski};
21use hoomd_vector::Angle;
22
23impl MaximumAllowableInteractionRange for EightEight {
24    /// The largest value that the maximum interaction range can take.
25    ///
26    /// This bound is determined by the edge length of the octagon.
27    #[inline]
28    fn maximum_allowable_interaction_range(&self) -> f64 {
29        EightEight::CUSP_TO_EDGE
30    }
31}
32
33impl Wrap<Point<Hyperbolic<3>>> for Periodic<EightEight> {
34    /// Wrap a point on the Hyperbolic to the inside of the {8,8} tile.
35    ///
36    /// Note that the function fails to wrap points that are outside the octagon
37    /// and further than `EightEight::EDGE_LENGTH`/2 from any of the vertices. In
38    /// this case, the function returns `Error::CannotWrapProperties`
39    ///
40    /// # Example
41    /// ```
42    /// use approxim::assert_relative_eq;
43    /// use hoomd_geometry::shape::EightEight;
44    /// use hoomd_manifold::Hyperbolic;
45    /// use hoomd_microstate::{
46    ///     boundary::{Periodic, Wrap},
47    ///     property::Point,
48    /// };
49    /// use std::f64::consts::PI;
50    ///
51    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
52    /// const EIGHTEIGHT: f64 = EightEight::EIGHTEIGHT;
53    /// let offset = PI / 8.0;
54    /// let boost = 2.0;
55    /// let point =
56    ///     Hyperbolic::<3>::from_polar_coordinates(boost, offset + PI / 4.0);
57    /// let point = Point::new(point);
58    /// let periodic = Periodic::new(0.5, EightEight {})?;
59    ///
60    /// let wrapped_point = periodic.wrap(point)?;
61    ///
62    /// let new_boost = 2.0
63    ///     * (EIGHTEIGHT.tanh()
64    ///         / (offset.cos() - offset.sin() * (1.0 - (2.0_f64).sqrt())))
65    ///     .atanh()
66    ///     - boost;
67    /// let ans = Hyperbolic::<3>::from_polar_coordinates(
68    ///     new_boost,
69    ///     6.0 * PI / 4.0 - offset,
70    /// );
71    /// assert_relative_eq!(
72    ///     ans.coordinates()[0],
73    ///     wrapped_point.position.coordinates()[0],
74    ///     epsilon = 1e-12
75    /// );
76    /// assert_relative_eq!(
77    ///     ans.coordinates()[1],
78    ///     wrapped_point.position.coordinates()[1],
79    ///     epsilon = 1e-12
80    /// );
81    /// assert_relative_eq!(
82    ///     ans.coordinates()[2],
83    ///     wrapped_point.position.coordinates()[2],
84    ///     epsilon = 1e-12
85    /// );
86    /// # Ok(())
87    /// # }
88    /// ```
89    #[inline]
90    #[expect(clippy::too_many_lines, reason = "complicated function")]
91    fn wrap(&self, properties: Point<Hyperbolic<3>>) -> Result<Point<Hyperbolic<3>>, Error> {
92        let mut properties = properties;
93        let r = properties.position_mut();
94        let angle = r.coordinates()[1]
95            .atan2(r.coordinates()[0])
96            .rem_euclid(2.0 * PI);
97
98        // distance to the boundary; if positive, r is within the tile and does not need to be wrapped
99        let d = EightEight::distance_to_boundary(r);
100        if d >= 0.0 {
101            Ok(properties)
102        } else if d > -self.maximum_interaction_range {
103            // get the sequence of transformations necessary to wrap point back into the tile
104            let nearest_vertex_number =
105                (((angle + (PI / 8.0)).rem_euclid(PI * 2.0)) / (PI / 4.0)).floor();
106
107            // transform point to frame where relevant vertex is in the center
108            let (vertex_boost, vertex_angle) = (
109                EightEight::EIGHTEIGHT,
110                (nearest_vertex_number * PI / 4.0).rem_euclid(PI * 2.0),
111            );
112            let (v_cosh, v_sinh, v_cos, v_sin) = (
113                (-vertex_boost).cosh(),
114                (-vertex_boost).sinh(),
115                (-vertex_angle).cos(),
116                (-vertex_angle).sin(),
117            );
118            let transformed_point = Minkowski::from([
119                r.coordinates()[0] * v_cosh * v_cos - r.coordinates()[1] * v_cosh * v_sin
120                    + r.coordinates()[2] * v_sinh,
121                r.coordinates()[0] * v_sin + r.coordinates()[1] * v_cos,
122                r.coordinates()[0] * v_sinh * v_cos - r.coordinates()[1] * v_sinh * v_sin
123                    + r.coordinates()[2] * v_cosh,
124            ]);
125            // get coords of point in transformed frame
126            let trans_angle =
127                transformed_point.coordinates[1].atan2(transformed_point.coordinates[0]);
128            // find which octant the transformed point is in
129            let octant = (((trans_angle + (PI / 8.0)).rem_euclid(2.0 * PI)) / (PI / 4.0)).floor();
130
131            // transform to tile
132            let eta = EightEight::CUSP_TO_EDGE;
133            let wrapped: [f64; 3];
134            match octant {
135                5.0 => {
136                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
137                    wrapped =
138                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
139                }
140                6.0 => {
141                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
142                    let wrapped_1 =
143                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
144                    let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
145                    wrapped = EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
146                }
147                7.0 => {
148                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
149                    let wrapped_1 =
150                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
151                    let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
152                    let wrapped_2 =
153                        EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
154                    let theta_3 = (theta_2 + 3.0).rem_euclid(8.0);
155                    wrapped = EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
156                }
157                3.0 => {
158                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
159                    wrapped =
160                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
161                }
162                2.0 => {
163                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
164                    let wrapped_1 =
165                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
166                    let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
167                    wrapped = EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
168                }
169                1.0 => {
170                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
171                    let wrapped_1 =
172                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
173                    let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
174                    let wrapped_2 =
175                        EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
176                    let theta_3 = (theta_2 - 3.0).rem_euclid(8.0);
177                    wrapped = EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
178                }
179                0.0 => {
180                    if transformed_point.coordinates[1] >= 0.0 {
181                        let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
182                        let wrapped_1 =
183                            EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
184                        let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
185                        let wrapped_2 =
186                            EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
187                        let theta_3 = (theta_2 - 3.0).rem_euclid(8.0);
188                        let wrapped_3 =
189                            EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
190                        let theta_4 = (theta_3 - 3.0).rem_euclid(8.0);
191                        wrapped = EightEight::gamma(eta, theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3);
192                    } else {
193                        let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
194                        let wrapped_1 =
195                            EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
196                        let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
197                        let wrapped_2 =
198                            EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
199                        let theta_3 = (theta_2 + 3.0).rem_euclid(8.0);
200                        let wrapped_3 =
201                            EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
202                        let theta_4 = (theta_3 + 3.0).rem_euclid(8.0);
203                        wrapped = EightEight::gamma(eta, theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3);
204                    }
205                }
206                _ => return Err(Error::CannotWrapProperties),
207            }
208            let wrapped_hyperbolic =
209                Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from(wrapped));
210            *r = wrapped_hyperbolic;
211            Ok(properties)
212        } else {
213            Err(Error::CannotWrapProperties)
214        }
215    }
216}
217
218impl Wrap<OrientedHyperbolicPoint<3, Angle>> for Periodic<EightEight> {
219    /// Wrap the positions and orientations of oriented bodies in two-dimensional
220    /// hyperbolic space under the {8,8} tiling.
221    #[inline]
222    #[allow(clippy::too_many_lines, reason = "complicated function")]
223    fn wrap(
224        &self,
225        properties: OrientedHyperbolicPoint<3, Angle>,
226    ) -> Result<OrientedHyperbolicPoint<3, Angle>, Error> {
227        let original_orientation = properties.orientation.theta;
228        let mut properties = properties;
229        // let orientation = properties.orientation_mut();
230        let r = properties.position_mut();
231        let angle = r.coordinates()[1]
232            .atan2(r.coordinates()[0])
233            .rem_euclid(2.0 * PI);
234
235        // distance to the boundary; if positive, r is within the tile and does not need to be wrapped
236        let d = EightEight::distance_to_boundary(r);
237        if d >= 0.0 {
238            Ok(properties)
239        } else if d > -self.maximum_interaction_range {
240            // get the sequence of transformations necessary to wrap point back into the tile
241            let nearest_vertex_number =
242                (((angle + (PI / 8.0)).rem_euclid(PI * 2.0)) / (PI / 4.0)).floor();
243
244            // transform point to frame where relevant vertex is in the center
245            let (vertex_boost, vertex_angle) = (
246                EightEight::EIGHTEIGHT,
247                (nearest_vertex_number * PI / 4.0).rem_euclid(PI * 2.0),
248            );
249            let (v_cosh, v_sinh, v_cos, v_sin) = (
250                (-vertex_boost).cosh(),
251                (-vertex_boost).sinh(),
252                (-vertex_angle).cos(),
253                (-vertex_angle).sin(),
254            );
255            let transformed_point = Minkowski::from([
256                r.coordinates()[0] * v_cosh * v_cos - r.coordinates()[1] * v_cosh * v_sin
257                    + r.coordinates()[2] * v_sinh,
258                r.coordinates()[0] * v_sin + r.coordinates()[1] * v_cos,
259                r.coordinates()[0] * v_sinh * v_cos - r.coordinates()[1] * v_sinh * v_sin
260                    + r.coordinates()[2] * v_cosh,
261            ]);
262            // get coords of point in transformed frame
263            let trans_angle =
264                transformed_point.coordinates[1].atan2(transformed_point.coordinates[0]);
265            // let trans_boost = (transformed_point.coordinates[2]).acosh();
266
267            // find which octant the transformed point is in
268            let octant = (((trans_angle + (PI / 8.0)).rem_euclid(2.0 * PI)) / (PI / 4.0)).floor();
269
270            // transform to tile
271            let eta = EightEight::CUSP_TO_EDGE;
272
273            let wrapped: [f64; 3];
274            let relative_angle: f64;
275            match octant {
276                5.0 => {
277                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
278                    wrapped =
279                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
280                    relative_angle =
281                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
282                }
283                6.0 => {
284                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
285                    let wrapped_1 =
286                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
287                    let relative_angle_1 =
288                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
289                    let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
290                    wrapped = EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
291                    relative_angle =
292                        EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1)
293                            + relative_angle_1;
294                }
295                7.0 => {
296                    let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
297                    let wrapped_1 =
298                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
299                    let relative_angle_1 =
300                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
301                    let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
302                    let wrapped_2 =
303                        EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
304                    let relative_angle_2 =
305                        EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
306                    let theta_3 = (theta_2 + 3.0).rem_euclid(8.0);
307                    wrapped = EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
308                    relative_angle =
309                        EightEight::reorient(theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2)
310                            + relative_angle_1
311                            + relative_angle_2;
312                }
313                3.0 => {
314                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
315                    wrapped =
316                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
317                    relative_angle =
318                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
319                }
320                2.0 => {
321                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
322                    let wrapped_1 =
323                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
324                    let relative_angle_1 =
325                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
326                    let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
327                    wrapped = EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
328                    relative_angle =
329                        EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1)
330                            + relative_angle_1;
331                }
332                1.0 => {
333                    let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
334                    let wrapped_1 =
335                        EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
336                    let relative_angle_1 =
337                        EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
338                    let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
339                    let wrapped_2 =
340                        EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
341                    let relative_angle_2 =
342                        EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
343                    let theta_3 = (theta_2 - 3.0).rem_euclid(8.0);
344                    wrapped = EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
345                    relative_angle =
346                        EightEight::reorient(theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2)
347                            + relative_angle_1
348                            + relative_angle_2;
349                }
350                0.0 => {
351                    if transformed_point.coordinates[1] >= 0.0 {
352                        let theta_1 = (nearest_vertex_number + 4.0).rem_euclid(8.0);
353                        let wrapped_1 =
354                            EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
355                        let relative_angle_1 =
356                            EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
357                        let theta_2 = (theta_1 - 3.0).rem_euclid(8.0);
358                        let wrapped_2 =
359                            EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
360                        let relative_angle_2 =
361                            EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
362                        let theta_3 = (theta_2 - 3.0).rem_euclid(8.0);
363                        let wrapped_3 =
364                            EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
365                        let relative_angle_3 =
366                            EightEight::reorient(theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
367                        let theta_4 = (theta_3 - 3.0).rem_euclid(8.0);
368                        wrapped = EightEight::gamma(eta, theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3);
369                        relative_angle =
370                            EightEight::reorient(theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3)
371                                + relative_angle_1
372                                + relative_angle_2
373                                + relative_angle_3;
374                    } else {
375                        let theta_1 = (nearest_vertex_number + 3.0).rem_euclid(8.0);
376                        let wrapped_1 =
377                            EightEight::gamma(eta, theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
378                        let relative_angle_1 =
379                            EightEight::reorient(theta_1 * PI / 4.0 + PI / 8.0, r.coordinates());
380                        let theta_2 = (theta_1 + 3.0).rem_euclid(8.0);
381                        let wrapped_2 =
382                            EightEight::gamma(eta, theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
383                        let relative_angle_2 =
384                            EightEight::reorient(theta_2 * PI / 4.0 + PI / 8.0, &wrapped_1);
385                        let theta_3 = (theta_2 + 3.0).rem_euclid(8.0);
386                        let wrapped_3 =
387                            EightEight::gamma(eta, theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
388                        let relative_angle_3 =
389                            EightEight::reorient(theta_3 * PI / 4.0 + PI / 8.0, &wrapped_2);
390                        let theta_4 = (theta_3 + 3.0).rem_euclid(8.0);
391                        wrapped = EightEight::gamma(eta, theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3);
392                        relative_angle =
393                            EightEight::reorient(theta_4 * PI / 4.0 + PI / 8.0, &wrapped_3)
394                                + relative_angle_1
395                                + relative_angle_2
396                                + relative_angle_3;
397                    }
398                }
399                _ => return Err(Error::CannotWrapProperties),
400            }
401            let wrapped_hyperbolic =
402                Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from(wrapped));
403            let mut final_orientation = relative_angle + original_orientation;
404            while final_orientation > PI {
405                final_orientation -= 2.0 * PI;
406            }
407            while final_orientation <= -PI {
408                final_orientation += 2.0 * PI;
409            }
410            *r = wrapped_hyperbolic;
411            *properties.orientation_mut() = Angle::from(final_orientation);
412            Ok(properties)
413        } else {
414            Err(Error::CannotWrapProperties)
415        }
416    }
417}
418
419impl GenerateGhosts<Point<Hyperbolic<3>>> for Periodic<EightEight> {
420    #[inline]
421    fn maximum_interaction_range(&self) -> f64 {
422        self.maximum_interaction_range
423    }
424    /// Place periodic images of sites near the edge of the periodic boundary
425    #[inline]
426    fn generate_ghosts(
427        &self,
428        site_properties: &Point<Hyperbolic<3>>,
429    ) -> ArrayVec<Point<Hyperbolic<3>>, MAX_GHOSTS> {
430        let mut result = ArrayVec::new();
431        let r = site_properties.position();
432
433        // transform to tile
434        let eta = EightEight::CUSP_TO_EDGE;
435        let gamma_pt = |theta: f64, point: &[f64; 3]| {
436            let ghost = EightEight::gamma(eta, theta, point);
437            let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(Minkowski::from(ghost));
438            let mut new_site = *site_properties;
439            *new_site.position_mut() = new_hyperbolic;
440            new_site
441        };
442        // identify which octant the point is in
443        let angle = (r.coordinates()[1].atan2(r.coordinates()[0])).rem_euclid(2.0 * PI);
444        let nearest_vertex_number =
445            (((angle + (PI / 8.0)).rem_euclid(PI * 2.0)) / (PI / 4.0)).floor();
446        let coords = *r.coordinates();
447
448        let theta_1a = (nearest_vertex_number + 4.0).rem_euclid(8.0);
449        let ghost_1a = gamma_pt(theta_1a * PI / 4.0 + PI / 8.0, &coords);
450        result.push(ghost_1a);
451        let theta_1b = (nearest_vertex_number + 3.0).rem_euclid(8.0);
452        let ghost_1b = gamma_pt(theta_1b * PI / 4.0 + PI / 8.0, &coords);
453        result.push(ghost_1b);
454
455        let theta_2a = (theta_1a - 3.0).rem_euclid(8.0);
456        let ghost_2a = gamma_pt(
457            theta_2a * PI / 4.0 + PI / 8.0,
458            ghost_1a.position.coordinates(),
459        );
460        result.push(ghost_2a);
461        let theta_2b = (theta_1b + 3.0).rem_euclid(8.0);
462        let ghost_2b = gamma_pt(
463            theta_2b * PI / 4.0 + PI / 8.0,
464            ghost_1b.position.coordinates(),
465        );
466        result.push(ghost_2b);
467
468        let theta_3a = (theta_2a - 3.0).rem_euclid(8.0);
469        let ghost_3a = gamma_pt(
470            theta_3a * PI / 4.0 + PI / 8.0,
471            ghost_2a.position.coordinates(),
472        );
473        result.push(ghost_3a);
474        let theta_3b = (theta_2b + 3.0).rem_euclid(8.0);
475        let ghost_3b = gamma_pt(
476            theta_3b * PI / 4.0 + PI / 8.0,
477            ghost_2b.position.coordinates(),
478        );
479        result.push(ghost_3b);
480
481        let theta_4 = (theta_3a - 3.0).rem_euclid(8.0);
482        let ghost_4 = gamma_pt(
483            theta_4 * PI / 4.0 + PI / 8.0,
484            ghost_3a.position.coordinates(),
485        );
486        result.push(ghost_4);
487
488        result
489    }
490}
491
492impl GenerateGhosts<OrientedHyperbolicPoint<3, Angle>> for Periodic<EightEight> {
493    #[inline]
494    fn maximum_interaction_range(&self) -> f64 {
495        self.maximum_interaction_range
496    }
497    /// Place periodic images of sites near the edge of the periodic boundary
498    #[inline]
499    #[expect(clippy::too_many_lines, reason = "complicated function")]
500    fn generate_ghosts(
501        &self,
502        site_properties: &OrientedHyperbolicPoint<3, Angle>,
503    ) -> ArrayVec<OrientedHyperbolicPoint<3, Angle>, MAX_GHOSTS> {
504        let mut result = ArrayVec::new();
505        let r = site_properties.position();
506        // identify which octant the point is in
507        let angle = (r.coordinates()[1].atan2(r.coordinates()[0])).rem_euclid(2.0 * PI);
508        let nearest_vertex_number =
509            (((angle + (PI / 8.0)).rem_euclid(PI * 2.0)) / (PI / 4.0)).floor();
510        let coords = *r.coordinates();
511        let orientation = site_properties.orientation.theta;
512        // transform to tile
513        let eta = EightEight::CUSP_TO_EDGE;
514        let gamma_pt = |theta: f64, point: &[f64; 3]| {
515            let ghost = EightEight::gamma(eta, theta, point);
516            let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(Minkowski::from(ghost));
517            let mut new_site = *site_properties;
518            *new_site.position_mut() = new_hyperbolic;
519            new_site
520        };
521
522        let theta_1a = (nearest_vertex_number + 4.0).rem_euclid(8.0);
523        let ghost_1a = gamma_pt(theta_1a * PI / 4.0 + PI / 8.0, &coords);
524        let rel_angle_1a = EightEight::reorient(theta_1a * PI / 4.0 + PI / 8.0, &coords);
525        let orientation_1a = rel_angle_1a + orientation;
526        let mut final_orientation = orientation_1a;
527        while final_orientation > PI {
528            final_orientation -= 2.0 * PI;
529        }
530        while final_orientation <= -PI {
531            final_orientation += 2.0 * PI;
532        }
533        let mut new_site_1a = *site_properties;
534        *new_site_1a.position_mut() = ghost_1a.position;
535        *new_site_1a.orientation_mut() = Angle::from(final_orientation);
536        result.push(new_site_1a);
537
538        let theta_1b = (nearest_vertex_number + 3.0).rem_euclid(8.0);
539        let ghost_1b = gamma_pt(theta_1b * PI / 4.0 + PI / 8.0, &coords);
540        let rel_angle_1b = EightEight::reorient(theta_1b * PI / 4.0 + PI / 8.0, &coords);
541        let orientation_1b = orientation + rel_angle_1b;
542        let mut final_orientation = orientation_1b;
543        while final_orientation > PI {
544            final_orientation -= 2.0 * PI;
545        }
546        while final_orientation <= -PI {
547            final_orientation += 2.0 * PI;
548        }
549        let mut new_site_1b = *site_properties;
550        *new_site_1b.position_mut() = ghost_1b.position;
551        *new_site_1b.orientation_mut() = Angle::from(final_orientation);
552        result.push(new_site_1b);
553
554        let theta_2a = (theta_1a - 3.0).rem_euclid(8.0);
555        let ghost_2a = gamma_pt(
556            theta_2a * PI / 4.0 + PI / 8.0,
557            ghost_1a.position.coordinates(),
558        );
559        let rel_angle_2a = EightEight::reorient(
560            theta_2a * PI / 4.0 + PI / 8.0,
561            ghost_1a.position.coordinates(),
562        );
563        let orientation_2a = orientation_1a + rel_angle_2a;
564        let mut final_orientation = orientation_2a;
565        while final_orientation > PI {
566            final_orientation -= 2.0 * PI;
567        }
568        while final_orientation <= -PI {
569            final_orientation += 2.0 * PI;
570        }
571        let mut new_site_2a = *site_properties;
572        *new_site_2a.position_mut() = ghost_2a.position;
573        *new_site_2a.orientation_mut() = Angle::from(final_orientation);
574        result.push(new_site_2a);
575
576        let theta_2b = (theta_1b + 3.0).rem_euclid(8.0);
577        let ghost_2b = gamma_pt(
578            theta_2b * PI / 4.0 + PI / 8.0,
579            ghost_1b.position.coordinates(),
580        );
581        let rel_angle_2b = EightEight::reorient(
582            theta_2b * PI / 4.0 + PI / 8.0,
583            ghost_1b.position.coordinates(),
584        );
585        let orientation_2b = orientation_1b + rel_angle_2b;
586        let mut final_orientation = orientation_2b;
587        while final_orientation > PI {
588            final_orientation -= 2.0 * PI;
589        }
590        while final_orientation <= -PI {
591            final_orientation += 2.0 * PI;
592        }
593        let mut new_site_2b = *site_properties;
594        *new_site_2b.position_mut() = ghost_2b.position;
595        *new_site_2b.orientation_mut() = Angle::from(final_orientation);
596        result.push(new_site_2b);
597
598        let theta_3a = (theta_2a - 3.0).rem_euclid(8.0);
599        let ghost_3a = gamma_pt(
600            theta_3a * PI / 4.0 + PI / 8.0,
601            ghost_2a.position.coordinates(),
602        );
603        let rel_angle_3a = EightEight::reorient(
604            theta_3a * PI / 4.0 + PI / 8.0,
605            ghost_2a.position.coordinates(),
606        );
607        let orientation_3a = orientation_2a + rel_angle_3a;
608        let mut final_orientation = orientation_3a;
609        while final_orientation > PI {
610            final_orientation -= 2.0 * PI;
611        }
612        while final_orientation <= -PI {
613            final_orientation += 2.0 * PI;
614        }
615        let mut new_site_3a = *site_properties;
616        *new_site_3a.position_mut() = ghost_3a.position;
617        *new_site_3a.orientation_mut() = Angle::from(final_orientation);
618        result.push(new_site_3a);
619
620        let theta_3b = (theta_2b + 3.0).rem_euclid(8.0);
621        let ghost_3b = gamma_pt(
622            theta_3b * PI / 4.0 + PI / 8.0,
623            ghost_2b.position.coordinates(),
624        );
625        let rel_angle_3b = EightEight::reorient(
626            theta_3b * PI / 4.0 + PI / 8.0,
627            ghost_2b.position.coordinates(),
628        );
629        let orientation_3b = rel_angle_3b + orientation_2b;
630        let mut final_orientation = orientation_3b;
631        while final_orientation > PI {
632            final_orientation -= 2.0 * PI;
633        }
634        while final_orientation <= -PI {
635            final_orientation += 2.0 * PI;
636        }
637        let mut new_site_3b = *site_properties;
638        *new_site_3b.position_mut() = ghost_3b.position;
639        *new_site_3b.orientation_mut() = Angle::from(final_orientation);
640        result.push(new_site_3b);
641
642        let theta_4 = (theta_3a - 3.0).rem_euclid(8.0);
643        let ghost_4 = gamma_pt(
644            theta_4 * PI / 4.0 + PI / 8.0,
645            ghost_3a.position.coordinates(),
646        );
647        let rel_angle_4 = EightEight::reorient(
648            theta_4 * PI / 4.0 + PI / 8.0,
649            ghost_3a.position.coordinates(),
650        );
651        let orientation_4 = orientation_3a + rel_angle_4;
652        let mut final_orientation = orientation_4;
653        while final_orientation > PI {
654            final_orientation -= 2.0 * PI;
655        }
656        while final_orientation <= -PI {
657            final_orientation += 2.0 * PI;
658        }
659        let mut new_site_4 = *site_properties;
660        *new_site_4.position_mut() = ghost_4.position;
661        *new_site_4.orientation_mut() = Angle::from(final_orientation);
662        result.push(new_site_4);
663
664        result
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use crate::property::{OrientedHyperbolicPoint, Point};
672    use approxim::assert_relative_eq;
673    use hoomd_manifold::{Hyperbolic, HyperbolicDisk};
674    use hoomd_vector::Metric;
675    use rand::{RngExt, SeedableRng, distr::Distribution, rngs::StdRng};
676    use std::f64::consts::PI;
677
678    #[test]
679    fn doesnt_wrap_if_inside() {
680        let r = 1.528_570_919_480_998;
681        let mut rng = StdRng::seed_from_u64(239);
682        let disk = HyperbolicDisk {
683            disk_radius: r.try_into().expect("hard-coded positive number"),
684            point: Hyperbolic::from_minkowski_coordinates(Minkowski::from([0.0, 0.0, 1.0])),
685        };
686        let random_point: Hyperbolic<3> = disk.sample(&mut rng);
687        let random_point = Point::new(random_point);
688
689        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
690        let wrapped_point = periodic.wrap(random_point).expect("hard-coded");
691        assert_eq!(
692            random_point.position.coordinates(),
693            wrapped_point.position.coordinates()
694        );
695    }
696
697    #[test]
698    fn wraps_to_opposite_edge() {
699        let mut rng = StdRng::seed_from_u64(1);
700        let side = f64::from(rng.random_range(0..8));
701        let boost = EightEight::CUSP_TO_EDGE + 0.5;
702        let offset = PI / 8.0;
703        let point = Hyperbolic::<3>::from_polar_coordinates(boost, side * PI / 4.0 + offset);
704        let point = Point::new(point);
705        let periodic = Periodic::new(1.0, EightEight {}).expect("hard-coded positive number");
706        let wrapped_point = periodic.wrap(point).expect("hard-coded");
707
708        let wrapped_side = (side + 4.0).rem_euclid(8.0);
709        let octant = (((wrapped_point.position.coordinates()[1]
710            .atan2(wrapped_point.position.coordinates()[0]))
711            / (PI / 4.0))
712            .floor())
713        .rem_euclid(8.0);
714
715        // Check that point is wrapped to correct octant
716        assert_eq!(wrapped_side, octant);
717
718        // Check that point mapping is correct
719        let new_boost = 2.0
720            * (EightEight::EIGHTEIGHT.tanh()
721                / (offset.cos() - offset.sin() * (1.0 - (2.0_f64).sqrt())))
722            .atanh()
723            - boost;
724        let ans = Hyperbolic::<3>::from_polar_coordinates(
725            new_boost,
726            (wrapped_side + 1.0) * (PI / 4.0) - offset,
727        );
728        assert_relative_eq!(ans, wrapped_point.position, epsilon = 1e-12);
729        assert_relative_eq!(
730            -EightEight::distance_to_boundary(&point.position),
731            EightEight::distance_to_boundary(&wrapped_point.position),
732            epsilon = 1e-12
733        );
734    }
735
736    #[test]
737    fn consistency_edge() {
738        let mut rng = StdRng::seed_from_u64(1);
739        let side = f64::from(rng.random_range(0..8));
740        let boost = EightEight::CUSP_TO_EDGE + 0.5;
741        let offset = PI / 8.0 + 0.15 * rng.random::<f64>();
742        let point = Hyperbolic::<3>::from_polar_coordinates(boost, side * PI / 4.0 + offset);
743        let point = Point::new(point);
744        let periodic = Periodic::new(1.0, EightEight {}).expect("hard-coded positive number");
745        let wrapped_point = periodic.wrap(point).expect("hard-coded");
746        let wrapped_poincare = wrapped_point.position.to_poincare();
747
748        // Check that mapping is consistent with Poincare transformation
749        let (q_u, q_v) = (
750            point.position.to_poincare()[0],
751            point.position.to_poincare()[1],
752        );
753        let phi = (side + 4.0).rem_euclid(8.0) * PI / 4.0 + PI / 8.0;
754        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
755        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi.cos());
756        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi.sin());
757        let pref = 1.0 / ((b * q_v - c * q_u).powi(2) + (a + b * q_u + c * q_v).powi(2));
758        let ans = [
759            pref * (q_u * (a.powi(2) + b.powi(2) - c.powi(2))
760                + 2.0 * b * c * q_v
761                + a * b * (1.0 + q_u.powi(2) + q_v.powi(2))),
762            pref * (q_v * (a.powi(2) - b.powi(2) + c.powi(2))
763                + 2.0 * b * c * q_u
764                + a * c * (1.0 + q_u.powi(2) + q_v.powi(2))),
765        ];
766
767        assert_relative_eq!(ans[0], wrapped_poincare[0], epsilon = 1e-12);
768        assert_relative_eq!(ans[1], wrapped_poincare[1], epsilon = 1e-12);
769    }
770
771    #[test]
772    fn wraps_to_opposite_edge_distance() {
773        let mut rng = StdRng::seed_from_u64(1);
774        let side = f64::from(rng.random_range(0..8));
775        let boost = EightEight::CUSP_TO_EDGE + 0.2;
776        let offset = PI / 8.0 + rng.random::<f64>() * (PI / 8.0) * (1.0 - 0.5);
777        let point = Hyperbolic::<3>::from_polar_coordinates(boost, side * PI / 4.0 + offset);
778        let point = Point::new(point);
779        let periodic = Periodic::new(1.0, EightEight {}).expect("hard-coded positive number");
780        let wrapped_point = periodic.wrap(point).expect("hard-coded");
781
782        // Check that mapping preserves the distance to the boundary
783        assert_relative_eq!(
784            -EightEight::distance_to_boundary(&point.position),
785            EightEight::distance_to_boundary(&wrapped_point.position),
786            epsilon = 1e-12
787        );
788    }
789
790    #[test]
791    fn wraps_orientation() {
792        let angle_offset: f64 = 0.3;
793        let boost = ((EightEight::EIGHTEIGHT).tanh() * ((PI / 4.0).sin())
794            / ((angle_offset).sin() + (PI / 4.0 - angle_offset).sin()))
795        .atanh()
796            + 0.1;
797        let point = Hyperbolic::<3>::from_polar_coordinates(boost, angle_offset + PI / 4.0);
798        let tangent = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
799            &Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, PI / 4.0),
800            &point,
801        );
802        let oriented_point = OrientedHyperbolicPoint {
803            position: point,
804            orientation: Angle::from(9.0 * PI / 8.0 + tangent),
805        };
806        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
807        let wrapped_point = periodic.wrap(oriented_point).expect("hard-coded");
808
809        let answer = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
810            &Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, 6.0 * PI / 4.0),
811            &wrapped_point.position,
812        );
813
814        // Check that orientation maps correctly
815        assert_relative_eq!(
816            wrapped_point.orientation.theta,
817            5.0 * PI / 8.0 + answer,
818            epsilon = 1e-12
819        );
820    }
821
822    #[test]
823    fn wraps_nearby_orientations() {
824        let offset = 0.000_001;
825        let angle = PI / 8.0 + offset;
826        let boost = ((EightEight::EIGHTEIGHT).tanh() * ((PI / 4.0).sin())
827            / ((angle).sin() + (PI / 4.0 - angle).sin()))
828        .atanh()
829            + 0.5;
830        let point_1 =
831            Hyperbolic::<3>::from_polar_coordinates(boost, 5.0 * PI / 4.0 + PI / 8.0 - offset);
832        let point_2 =
833            Hyperbolic::<3>::from_polar_coordinates(boost, 5.0 * PI / 4.0 + PI / 8.0 + offset);
834
835        let oriented_point_1 = OrientedHyperbolicPoint {
836            position: point_1,
837            orientation: Angle::from(PI),
838        };
839        let oriented_point_2 = OrientedHyperbolicPoint {
840            position: point_2,
841            orientation: Angle::from(PI),
842        };
843        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
844        let wrapped_point_1 = periodic.wrap(oriented_point_1).expect("hard-coded");
845        let wrapped_point_2 = periodic.wrap(oriented_point_2).expect("hard-coded");
846
847        // check that positions are nearby
848        assert_relative_eq!(
849            wrapped_point_1.position.coordinates()[0],
850            wrapped_point_2.position.coordinates()[0],
851            epsilon = 1e-5
852        );
853        assert_relative_eq!(
854            wrapped_point_1.position.coordinates()[1],
855            wrapped_point_2.position.coordinates()[1],
856            epsilon = 1e-5
857        );
858        assert_relative_eq!(
859            wrapped_point_1.position.coordinates()[2],
860            wrapped_point_2.position.coordinates()[2],
861            epsilon = 1e-5
862        );
863
864        // check that orientations are nearby
865        assert_relative_eq!(
866            wrapped_point_1.orientation.theta.rem_euclid(2.0 * PI),
867            wrapped_point_2.orientation.theta.rem_euclid(2.0 * PI),
868            epsilon = 1e-5
869        );
870    }
871
872    #[test]
873    fn wraps_vertex() {
874        let boost = EightEight::EIGHTEIGHT + 0.4;
875        let point = Hyperbolic::<3>::from_polar_coordinates(boost, 3.0 * PI / 4.0 - 0.01);
876        let point = Point::new(point);
877        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
878        let wrapped_point = periodic.wrap(point).expect("hard-coded");
879        let wrapped_poincare = wrapped_point.position.to_poincare();
880        let point_poincare = point.position.to_poincare();
881
882        let phi7 = 7.0 * PI / 4.0 + PI / 8.0;
883        let a7 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
884        let b7 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi7.cos());
885        let c7 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi7.sin());
886        let pref_7 = 1.0
887            / ((b7 * point_poincare[1] - c7 * point_poincare[0]).powi(2)
888                + (a7 + b7 * point_poincare[0] + c7 * point_poincare[1]).powi(2));
889        let ans_7 = [
890            pref_7
891                * (point_poincare[0] * (a7.powi(2) + b7.powi(2) - c7.powi(2))
892                    + 2.0 * b7 * c7 * point_poincare[1]
893                    + a7 * b7 * (1.0 + point_poincare[0].powi(2) + point_poincare[1].powi(2))),
894            pref_7
895                * (point_poincare[1] * (a7.powi(2) - b7.powi(2) + c7.powi(2))
896                    + 2.0 * b7 * c7 * point_poincare[0]
897                    + a7 * c7 * (1.0 + point_poincare[0].powi(2) + point_poincare[1].powi(2))),
898        ];
899        let phi4 = PI + PI / 8.0;
900        let a4 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
901        let b4 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi4.cos());
902        let c4 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi4.sin());
903        let pref_4 = 1.0
904            / ((b4 * ans_7[1] - c4 * ans_7[0]).powi(2)
905                + (a4 + b4 * ans_7[0] + c4 * ans_7[1]).powi(2));
906        let ans_4 = [
907            pref_4
908                * (ans_7[0] * (a4.powi(2) + b4.powi(2) - c4.powi(2))
909                    + 2.0 * b4 * c4 * ans_7[1]
910                    + a4 * b4 * (1.0 + ans_7[0].powi(2) + ans_7[1].powi(2))),
911            pref_4
912                * (ans_7[1] * (a4.powi(2) - b4.powi(2) + c4.powi(2))
913                    + 2.0 * b4 * c4 * ans_7[0]
914                    + a4 * c4 * (1.0 + ans_7[0].powi(2) + ans_7[1].powi(2))),
915        ];
916        let phi1 = PI / 4.0 + PI / 8.0;
917        let a1 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
918        let b1 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi1.cos());
919        let c1 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi1.sin());
920        let pref_1 = 1.0
921            / ((b1 * ans_4[1] - c1 * ans_4[0]).powi(2)
922                + (a1 + b1 * ans_4[0] + c1 * ans_4[1]).powi(2));
923        let ans_1 = [
924            pref_1
925                * (ans_4[0] * (a1.powi(2) + b1.powi(2) - c1.powi(2))
926                    + 2.0 * b1 * c1 * ans_4[1]
927                    + a1 * b1 * (1.0 + ans_4[0].powi(2) + ans_4[1].powi(2))),
928            pref_1
929                * (ans_4[1] * (a1.powi(2) - b1.powi(2) + c1.powi(2))
930                    + 2.0 * b1 * c1 * ans_4[0]
931                    + a1 * c1 * (1.0 + ans_4[0].powi(2) + ans_4[1].powi(2))),
932        ];
933        let phi6 = 6.0 * PI / 4.0 + PI / 8.0;
934        let a6 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
935        let b6 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.cos());
936        let c6 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.sin());
937        let pref_6 = 1.0
938            / ((b6 * ans_1[1] - c6 * ans_1[0]).powi(2)
939                + (a6 + b6 * ans_1[0] + c6 * ans_1[1]).powi(2));
940        let ans_6 = [
941            pref_6
942                * (ans_1[0] * (a6.powi(2) + b6.powi(2) - c6.powi(2))
943                    + 2.0 * b6 * c6 * ans_1[1]
944                    + a6 * b6 * (1.0 + ans_1[0].powi(2) + ans_1[1].powi(2))),
945            pref_6
946                * (ans_1[1] * (a6.powi(2) - b6.powi(2) + c6.powi(2))
947                    + 2.0 * b6 * c6 * ans_1[0]
948                    + a6 * c6 * (1.0 + ans_1[0].powi(2) + ans_1[1].powi(2))),
949        ];
950
951        assert_relative_eq!(ans_6[0], wrapped_poincare[0], epsilon = 1e-12);
952        assert_relative_eq!(ans_6[1], wrapped_poincare[1], epsilon = 1e-12);
953    }
954
955    #[test]
956    fn wraps_vertex_orientation() {
957        let boost = EightEight::EIGHTEIGHT + 0.5;
958        let ve = EightEight::EIGHTEIGHT;
959        let point = Hyperbolic::<3>::from_polar_coordinates(boost, PI / 4.0 + 0.00001);
960
961        let (int_1, _or1) =
962            OrientedHyperbolicPoint::<3, Angle>::intersection_point(PI / 8.0, ve - boost);
963        let intersection = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
964            (ve.cosh()) * (int_1.sinh()) * ((PI / 4.0).cos()) * ((PI / 8.0).cos())
965                - (int_1.sinh()) * ((PI / 4.0).sin()) * ((PI / 8.0).sin())
966                + (ve.sinh()) * (int_1.cosh()) * ((PI / 4.0).cos()),
967            (ve.cosh()) * (int_1.sinh()) * ((PI / 4.0).sin()) * ((PI / 8.0).cos())
968                + (int_1.sinh()) * ((PI / 4.0).cos()) * ((PI / 8.0).sin())
969                + (ve.sinh()) * (int_1.cosh()) * ((PI / 4.0).sin()),
970            (ve.sinh()) * (int_1.sinh()) * ((PI / 8.0).cos()) + (ve.cosh()) * (int_1.cosh()),
971        ]));
972        let pt_v_to_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
973            &Hyperbolic::<3>::from_polar_coordinates(ve, PI / 4.0),
974            &intersection,
975        );
976        let pt_int_to_pnt =
977            OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(&intersection, &point);
978
979        let oriented_point = OrientedHyperbolicPoint {
980            position: point,
981            orientation: Angle::from(pt_v_to_int + pt_int_to_pnt + 3.0 * PI / 8.0),
982        };
983        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
984        let wrapped_point = periodic.wrap(oriented_point).expect("hard-coded");
985
986        // check that the orientation maps correctly
987        let new_intersection = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
988            (ve.cosh()) * (int_1.sinh()) * ((5.0 * PI / 4.0).cos()) * ((9.0 * PI / 8.0).cos())
989                - (int_1.sinh()) * ((5.0 * PI / 4.0).sin()) * ((9.0 * PI / 8.0).sin())
990                + (ve.sinh()) * (int_1.cosh()) * ((5.0 * PI / 4.0).cos()),
991            (ve.cosh()) * (int_1.sinh()) * ((5.0 * PI / 4.0).sin()) * ((9.0 * PI / 8.0).cos())
992                + (int_1.sinh()) * ((5.0 * PI / 4.0).cos()) * ((9.0 * PI / 8.0).sin())
993                + (ve.sinh()) * (int_1.cosh()) * ((5.0 * PI / 4.0).sin()),
994            (ve.sinh()) * (int_1.sinh()) * ((9.0 * PI / 8.0).cos()) + (ve.cosh()) * (int_1.cosh()),
995        ]));
996        let pt_v_to_n_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
997            &Hyperbolic::<3>::from_polar_coordinates(ve, 5.0 * PI / 4.0),
998            &new_intersection,
999        );
1000        let pt_n_int_to_w_pnt = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1001            &new_intersection,
1002            &wrapped_point.position,
1003        );
1004        assert_relative_eq!(
1005            3.0 * PI / 8.0 + pt_v_to_n_int + pt_n_int_to_w_pnt,
1006            wrapped_point.orientation.theta,
1007            epsilon = 1e-12
1008        );
1009    }
1010    #[test]
1011    fn wraps_vertex_non_center() {
1012        let offset_boost: f64 = 0.4;
1013        let v: f64 = 2.448_452_447_678_076;
1014        let point = Hyperbolic::from_minkowski_coordinates(
1015            [
1016                (v.sinh()) * (offset_boost.cosh()),
1017                -offset_boost.sinh(),
1018                (v.cosh()) * (offset_boost.cosh()),
1019            ]
1020            .into(),
1021        );
1022        let point = Point::new(point);
1023        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1024        let wrapped_point = periodic.wrap(point).expect("hard-coded");
1025
1026        let wrapped_poincare = wrapped_point.position.to_poincare();
1027        let point_poincare = point.position.to_poincare();
1028
1029        let phi3 = 3.0 * PI / 4.0 + PI / 8.0;
1030        let a3 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1031        let b3 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.cos());
1032        let c3 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.sin());
1033        let pref_3 = 1.0
1034            / ((b3 * point_poincare[1] - c3 * point_poincare[0]).powi(2)
1035                + (a3 + b3 * point_poincare[0] + c3 * point_poincare[1]).powi(2));
1036        let ans_3 = [
1037            pref_3
1038                * (point_poincare[0] * (a3.powi(2) + b3.powi(2) - c3.powi(2))
1039                    + 2.0 * b3 * c3 * point_poincare[1]
1040                    + a3 * b3 * (1.0 + point_poincare[0].powi(2) + point_poincare[1].powi(2))),
1041            pref_3
1042                * (point_poincare[1] * (a3.powi(2) - b3.powi(2) + c3.powi(2))
1043                    + 2.0 * b3 * c3 * point_poincare[0]
1044                    + a3 * c3 * (1.0 + point_poincare[0].powi(2) + point_poincare[1].powi(2))),
1045        ];
1046        let phi6 = 6.0 * PI / 4.0 + PI / 8.0;
1047        let a6 = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1048        let b6 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.cos());
1049        let c6 = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.sin());
1050        let pref_6 = 1.0
1051            / ((b6 * ans_3[1] - c6 * ans_3[0]).powi(2)
1052                + (a6 + b6 * ans_3[0] + c6 * ans_3[1]).powi(2));
1053        let ans_6 = [
1054            pref_6
1055                * (ans_3[0] * (a6.powi(2) + b6.powi(2) - c6.powi(2))
1056                    + 2.0 * b6 * c6 * ans_3[1]
1057                    + a6 * b6 * (1.0 + ans_3[0].powi(2) + ans_3[1].powi(2))),
1058            pref_6
1059                * (ans_3[1] * (a6.powi(2) - b6.powi(2) + c6.powi(2))
1060                    + 2.0 * b6 * c6 * ans_3[0]
1061                    + a6 * c6 * (1.0 + ans_3[0].powi(2) + ans_3[1].powi(2))),
1062        ];
1063
1064        assert_relative_eq!(ans_6[0], wrapped_poincare[0], epsilon = 1e-12);
1065        assert_relative_eq!(ans_6[1], wrapped_poincare[1], epsilon = 1e-12);
1066    }
1067    #[test]
1068    fn wraps_orientation_vertex_non_center() {
1069        let offset_boost: f64 = 0.2;
1070        let angle_offset = 0.01;
1071        let v: f64 = 2.448_452_447_678_076;
1072        let point = Hyperbolic::from_minkowski_coordinates(
1073            [
1074                (v.sinh()) * (offset_boost.cosh())
1075                    + (v.cosh()) * (offset_boost.sinh()) * ((angle_offset + 3.0 * PI / 2.0).cos()),
1076                (offset_boost.sinh()) * ((angle_offset + 3.0 * PI / 2.0).sin()),
1077                (v.cosh()) * (offset_boost.cosh())
1078                    + (v.sinh()) * (offset_boost.sinh()) * ((angle_offset + 3.0 * PI / 2.0).cos()),
1079            ]
1080            .into(),
1081        );
1082
1083        let (int_1, _or1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1084            PI / 8.0 + angle_offset,
1085            offset_boost,
1086        );
1087
1088        let intersection = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1089            (v.cosh()) * (int_1.sinh()) * ((11.0 * PI / 8.0).cos()) + (v.sinh()) * (int_1.cosh()),
1090            (int_1.sinh()) * ((11.0 * PI / 8.0).sin()),
1091            (v.sinh()) * (int_1.sinh()) * ((11.0 * PI / 8.0).cos()) + (v.cosh()) * (int_1.cosh()),
1092        ]));
1093        let pt_v_to_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1094            &Hyperbolic::<3>::from_polar_coordinates(v, 0.0),
1095            &intersection,
1096        );
1097        let pt_int_to_pnt =
1098            OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(&intersection, &point);
1099
1100        let oriented_point = OrientedHyperbolicPoint {
1101            position: point,
1102            orientation: Angle::from(pt_v_to_int + pt_int_to_pnt + 11.0 * PI / 8.0),
1103        };
1104
1105        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1106        let wrapped_point = periodic.wrap(oriented_point).expect("hard-coded");
1107
1108        // check that wrapping correctly maps orientation
1109        let new_intersection = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1110            (v.cosh()) * (int_1.sinh()) * ((3.0 * PI / 2.0).cos()) * ((7.0 * PI / 8.0).cos())
1111                - (int_1.sinh()) * ((3.0 * PI / 2.0).sin()) * ((7.0 * PI / 8.0).sin())
1112                + (v.sinh()) * (int_1.cosh()) * ((3.0 * PI / 2.0).cos()),
1113            (v.cosh()) * (int_1.sinh()) * ((3.0 * PI / 2.0).sin()) * ((7.0 * PI / 8.0).cos())
1114                + (int_1.sinh()) * ((3.0 * PI / 2.0).cos()) * ((7.0 * PI / 8.0).sin())
1115                + (v.sinh()) * (int_1.cosh()) * ((3.0 * PI / 2.0).sin()),
1116            (v.sinh()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_1.cosh()),
1117        ]));
1118        let pt_v_to_n_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1119            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 2.0),
1120            &new_intersection,
1121        );
1122        let pt_n_int_to_w_pnt = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1123            &new_intersection,
1124            &wrapped_point.position,
1125        );
1126        assert_relative_eq!(
1127            3.0 * PI / 8.0 + pt_v_to_n_int + pt_n_int_to_w_pnt,
1128            wrapped_point.orientation.theta,
1129            epsilon = 1e-12
1130        );
1131    }
1132    #[test]
1133    fn ghost_near_side() {
1134        let mut rng = StdRng::seed_from_u64(1);
1135        let side = f64::from(rng.random_range(0..8));
1136        let offset = 0.4;
1137        let boost = 1.528_570_919_480_998 - offset;
1138        let point = Hyperbolic::<3>::from_polar_coordinates(boost, PI / 8.0 + side * PI / 4.0);
1139        let point = Point::new(point);
1140
1141        let periodic = Periodic::new(1.0, EightEight {}).expect("hard-coded positive number");
1142
1143        let ghost_array = periodic.generate_ghosts(&point);
1144        let ghost = match side {
1145            4.0 => ghost_array[0],
1146            _ => ghost_array[1],
1147        };
1148
1149        let ans = Hyperbolic::<3>::from_polar_coordinates(
1150            1.528_570_919_480_998 + offset,
1151            (side + 4.0).rem_euclid(8.0) * PI / 4.0 + PI / 8.0,
1152        );
1153
1154        assert_relative_eq!(ans, ghost.position, epsilon = 1e-12);
1155        assert_relative_eq!(
1156            EightEight::distance_to_boundary(&ghost.position),
1157            -EightEight::distance_to_boundary(&point.position),
1158            epsilon = 1e-12
1159        );
1160    }
1161
1162    #[test]
1163    fn ghost_near_vertex() {
1164        let offset_boost = 0.3;
1165        let v: f64 = 2.448_452_447_678_076;
1166        let point = Hyperbolic::<3>::from_polar_coordinates(v - offset_boost, 0.0);
1167        let point = Point::new(point);
1168        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1169
1170        let ghost_array: ArrayVec<Point<Hyperbolic<3>>, 8> = periodic.generate_ghosts(&point);
1171        let ghost_6 = ghost_array[6];
1172
1173        let ans_6 = Hyperbolic::<3>::from_polar_coordinates(v + offset_boost, PI);
1174        assert_relative_eq!(ans_6, ghost_6.position, epsilon = 1e-12);
1175
1176        let ghost_3 = ghost_array[3];
1177
1178        let ans_3 = Hyperbolic::from_minkowski_coordinates(
1179            [
1180                offset_boost.sinh(),
1181                -(v.sinh()) * (offset_boost.cosh()),
1182                (v.cosh()) * (offset_boost.cosh()),
1183            ]
1184            .into(),
1185        );
1186        assert_relative_eq!(ans_3, ghost_3.position, epsilon = 1e-12);
1187    }
1188
1189    #[test]
1190    #[allow(clippy::too_many_lines, reason = "complicated function")]
1191    fn consistency_vertex() {
1192        let offset_boost = 0.3;
1193        let offset_angle = 0.1;
1194        let edge_boost: f64 = 2.448_452_447_678_076;
1195        let point =
1196            Hyperbolic::<3>::from_polar_coordinates(edge_boost - offset_boost, offset_angle);
1197        let point = Point::new(point);
1198        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1199
1200        let ghost_array: ArrayVec<Point<Hyperbolic<3>>, 8> = periodic.generate_ghosts(&point);
1201
1202        // check double transformations
1203        let ghost_2_poincare = ghost_array[2].position.to_poincare();
1204        let (q_u, q_v) = (
1205            point.position.to_poincare()[0],
1206            point.position.to_poincare()[1],
1207        );
1208        let phi1 = PI / 4.0 + PI / 8.0;
1209        let phi4 = PI + PI / 8.0;
1210        let a = (1.0 + (PI / 4.0).cos() + 2.0 * ((PI / 4.0).cos()) * ((phi1 - phi4).cos()))
1211            / (1.0 - (PI / 4.0).cos());
1212        let d = (2.0 * ((PI / 4.0).cos()) * (phi1 - phi4).sin()) / (1.0 - (PI / 4.0).cos());
1213        let b = ((2.0 * ((PI / 4.0).cos()) * (1.0 + (PI / 4.0).cos())).sqrt())
1214            * (phi4.cos() + phi1.cos())
1215            / (1.0 - (PI / 4.0).cos());
1216        let c = ((2.0 * ((PI / 4.0).cos()) * (1.0 + (PI / 4.0).cos())).sqrt())
1217            * (phi4.sin() + phi1.sin())
1218            / (1.0 - (PI / 4.0).cos());
1219        let pref = 1.0 / ((b * q_v - c * q_u - d).powi(2) + (a + b * q_u + c * q_v).powi(2));
1220        let ans_2 = [
1221            pref * ((a * b - c * d) * (1.0 + q_u.powi(2) + q_v.powi(2))
1222                + q_u * (a.powi(2) + b.powi(2) - c.powi(2) - d.powi(2))
1223                + 2.0 * b * c * q_v
1224                - 2.0 * a * d * q_v),
1225            pref * ((a * c + b * d) * (1.0 + q_u.powi(2) + q_v.powi(2))
1226                + q_v * (a.powi(2) - b.powi(2) + c.powi(2) - d.powi(2))
1227                + 2.0 * b * c * q_u
1228                + 2.0 * a * d * q_u),
1229        ];
1230        assert_relative_eq!(ans_2[0], ghost_2_poincare[0], epsilon = 1e-12);
1231        assert_relative_eq!(ans_2[1], ghost_2_poincare[1], epsilon = 1e-12);
1232
1233        let ghost_3_poincare = ghost_array[3].position.to_poincare();
1234        let phi3 = 3.0 * PI / 4.0 + PI / 8.0;
1235        let phi6 = 6.0 * PI / 4.0 + PI / 8.0;
1236        let a = (1.0 + (PI / 4.0).cos() + 2.0 * ((PI / 4.0).cos()) * ((phi3 - phi6).cos()))
1237            / (1.0 - (PI / 4.0).cos());
1238        let d = (2.0 * ((PI / 4.0).cos()) * (phi6 - phi3).sin()) / (1.0 - (PI / 4.0).cos());
1239        let b = ((2.0 * ((PI / 4.0).cos()) * (1.0 + (PI / 4.0).cos())).sqrt())
1240            * (phi6.cos() + phi3.cos())
1241            / (1.0 - (PI / 4.0).cos());
1242        let c = ((2.0 * ((PI / 4.0).cos()) * (1.0 + (PI / 4.0).cos())).sqrt())
1243            * (phi6.sin() + phi3.sin())
1244            / (1.0 - (PI / 4.0).cos());
1245        let pref = 1.0 / ((b * q_v - c * q_u - d).powi(2) + (a + b * q_u + c * q_v).powi(2));
1246        let ans_3 = [
1247            pref * ((a * b - c * d) * (1.0 + q_u.powi(2) + q_v.powi(2))
1248                + q_u * (a.powi(2) + b.powi(2) - c.powi(2) - d.powi(2))
1249                + 2.0 * b * c * q_v
1250                - 2.0 * a * d * q_v),
1251            pref * ((a * c + b * d) * (1.0 + q_u.powi(2) + q_v.powi(2))
1252                + q_v * (a.powi(2) - b.powi(2) + c.powi(2) - d.powi(2))
1253                + 2.0 * b * c * q_u
1254                + 2.0 * a * d * q_u),
1255        ];
1256        assert_relative_eq!(ans_3[0], ghost_3_poincare[0], epsilon = 1e-12);
1257        assert_relative_eq!(ans_3[1], ghost_3_poincare[1], epsilon = 1e-12);
1258
1259        // check triple transformations
1260        let ghost_4_poincare = ghost_array[4].position.to_poincare();
1261        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1262        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.cos());
1263        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi6.sin());
1264        let pref = 1.0
1265            / ((b * ans_2[1] - c * ans_2[0]).powi(2) + (a + b * ans_2[0] + c * ans_2[1]).powi(2));
1266        let ans_4 = [
1267            pref * (ans_2[0] * (a.powi(2) + b.powi(2) - c.powi(2))
1268                + 2.0 * b * c * ans_2[1]
1269                + a * b * (1.0 + ans_2[0].powi(2) + ans_2[1].powi(2))),
1270            pref * (ans_2[1] * (a.powi(2) - b.powi(2) + c.powi(2))
1271                + 2.0 * b * c * ans_2[0]
1272                + a * c * (1.0 + ans_2[0].powi(2) + ans_2[1].powi(2))),
1273        ];
1274        assert_relative_eq!(ans_4[0], ghost_4_poincare[0], epsilon = 1e-12);
1275        assert_relative_eq!(ans_4[1], ghost_4_poincare[1], epsilon = 1e-12);
1276
1277        let ghost_5_poincare = ghost_array[5].position.to_poincare();
1278        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1279        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi1.cos());
1280        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi1.sin());
1281        let pref = 1.0
1282            / ((b * ans_3[1] - c * ans_3[0]).powi(2) + (a + b * ans_3[0] + c * ans_3[1]).powi(2));
1283        let ans_5 = [
1284            pref * (ans_3[0] * (a.powi(2) + b.powi(2) - c.powi(2))
1285                + 2.0 * b * c * ans_3[1]
1286                + a * b * (1.0 + ans_3[0].powi(2) + ans_3[1].powi(2))),
1287            pref * (ans_3[1] * (a.powi(2) - b.powi(2) + c.powi(2))
1288                + 2.0 * b * c * ans_3[0]
1289                + a * c * (1.0 + ans_3[0].powi(2) + ans_3[1].powi(2))),
1290        ];
1291        assert_relative_eq!(ans_5[0], ghost_5_poincare[0], epsilon = 1e-12);
1292        assert_relative_eq!(ans_5[1], ghost_5_poincare[1], epsilon = 1e-12);
1293
1294        // check quadruple transformation
1295        let ghost_6_poincare = ghost_array[6].position.to_poincare();
1296        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1297        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.cos());
1298        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.sin());
1299        let pref = 1.0
1300            / ((b * ans_4[1] - c * ans_4[0]).powi(2) + (a + b * ans_4[0] + c * ans_4[1]).powi(2));
1301        let ans_6 = [
1302            pref * (ans_4[0] * (a.powi(2) + b.powi(2) - c.powi(2))
1303                + 2.0 * b * c * ans_4[1]
1304                + a * b * (1.0 + ans_4[0].powi(2) + ans_4[1].powi(2))),
1305            pref * (ans_4[1] * (a.powi(2) - b.powi(2) + c.powi(2))
1306                + 2.0 * b * c * ans_4[0]
1307                + a * c * (1.0 + ans_4[0].powi(2) + ans_4[1].powi(2))),
1308        ];
1309        assert_relative_eq!(ans_6[0], ghost_6_poincare[0], epsilon = 1e-12);
1310        assert_relative_eq!(ans_6[1], ghost_6_poincare[1], epsilon = 1e-12);
1311
1312        // check single transformations
1313        let ghost_1_poincare = ghost_array[1].position.to_poincare();
1314        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1315        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.cos());
1316        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi3.sin());
1317        let pref = 1.0 / ((b * q_v - c * q_u).powi(2) + (a + b * q_u + c * q_v).powi(2));
1318        let ans_1 = [
1319            pref * (q_u * (a.powi(2) + b.powi(2) - c.powi(2))
1320                + 2.0 * b * c * q_v
1321                + a * b * (1.0 + q_u.powi(2) + q_v.powi(2))),
1322            pref * (q_v * (a.powi(2) - b.powi(2) + c.powi(2))
1323                + 2.0 * b * c * q_u
1324                + a * c * (1.0 + q_u.powi(2) + q_v.powi(2))),
1325        ];
1326        assert_relative_eq!(ans_1[0], ghost_1_poincare[0], epsilon = 1e-12);
1327        assert_relative_eq!(ans_1[1], ghost_1_poincare[1], epsilon = 1e-12);
1328
1329        let ghost_0_poincare = ghost_array[0].position.to_poincare();
1330        let a = ((1.0 + (PI / 4.0).cos()) / (1.0 - (PI / 4.0).cos())).sqrt();
1331        let b = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi4.cos());
1332        let c = ((2.0 * ((PI / 4.0).cos())) / (1.0 - (PI / 4.0).cos())).sqrt() * (phi4.sin());
1333        let pref = 1.0 / ((b * q_v - c * q_u).powi(2) + (a + b * q_u + c * q_v).powi(2));
1334        let ans_0 = [
1335            pref * (q_u * (a.powi(2) + b.powi(2) - c.powi(2))
1336                + 2.0 * b * c * q_v
1337                + a * b * (1.0 + q_u.powi(2) + q_v.powi(2))),
1338            pref * (q_v * (a.powi(2) - b.powi(2) + c.powi(2))
1339                + 2.0 * b * c * q_u
1340                + a * c * (1.0 + q_u.powi(2) + q_v.powi(2))),
1341        ];
1342        assert_relative_eq!(ans_0[0], ghost_0_poincare[0], epsilon = 1e-12);
1343        assert_relative_eq!(ans_0[1], ghost_0_poincare[1], epsilon = 1e-12);
1344    }
1345
1346    #[test]
1347    #[allow(clippy::too_many_lines, reason = "complicated function")]
1348    fn ghost_near_zeroth_vertex_orientation() {
1349        let offset_boost = 0.3;
1350        let v: f64 = 2.448_452_447_678_076;
1351        let point = Hyperbolic::<3>::from_polar_coordinates(v - offset_boost, 0.1);
1352        let point = OrientedHyperbolicPoint {
1353            position: point,
1354            orientation: Angle::from(0.0),
1355        };
1356
1357        // get nearest boundary point
1358        let point_in_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1359            (v.cosh()) * point.position.coordinates()[0]
1360                - (v.sinh()) * point.position.coordinates()[2],
1361            point.position.coordinates()[1],
1362            -(v.sinh()) * point.position.coordinates()[0]
1363                + (v.cosh()) * point.position.coordinates()[2],
1364        ]));
1365        let (angle_c, boost_c) = (
1366            point_in_center.coordinates()[1]
1367                .atan2(point_in_center.coordinates()[0])
1368                .rem_euclid(2.0 * PI),
1369            (point_in_center.coordinates()[2]).acosh(),
1370        );
1371        let (int_o, _o_o) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1372            angle_c - 7.0 * PI / 8.0,
1373            boost_c,
1374        );
1375        // have confirmed that this is the shortest path from the point to the boundary
1376        let intersection_o = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1377            (v.cosh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.sinh()) * (int_o.cosh()),
1378            (int_o.sinh()) * ((7.0 * PI / 8.0).sin()),
1379            (v.sinh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_o.cosh()),
1380        ]));
1381
1382        let int_to_v_o = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1383            &Hyperbolic::<3>::from_polar_coordinates(v, 0.0),
1384            &intersection_o,
1385        );
1386        let tang_o = 7.0 * PI / 8.0 + int_to_v_o;
1387        let relative_orientation_upper =
1388            (OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1389                &point.position,
1390                &intersection_o,
1391            ) - tang_o)
1392                .rem_euclid(2.0 * PI);
1393
1394        let (int_ol, _o_ol) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1395            9.0 * PI / 8.0 - angle_c,
1396            boost_c,
1397        );
1398
1399        let intersection_ol = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1400            (v.cosh()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.sinh()) * (int_ol.cosh()),
1401            (int_ol.sinh()) * ((9.0 * PI / 8.0).sin()),
1402            (v.sinh()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.cosh()) * (int_ol.cosh()),
1403        ]));
1404
1405        let int_to_v_ol = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1406            &Hyperbolic::<3>::from_polar_coordinates(v, 0.0),
1407            &intersection_ol,
1408        );
1409        let tang_ol = 9.0 * PI / 8.0 + int_to_v_ol;
1410        let relative_orientation_lower =
1411            (OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1412                &point.position,
1413                &intersection_ol,
1414            ) - tang_ol)
1415                .rem_euclid(2.0 * PI);
1416
1417        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1418
1419        let ghost_array = periodic.generate_ghosts(&point);
1420
1421        let ghost_0 = ghost_array[0];
1422        let ghost_0_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1423            (v.cosh()) * ((5.0 * PI / 4.0).cos()) * ghost_0.position.coordinates()[0]
1424                + (v.cosh()) * ((5.0 * PI / 4.0).sin()) * ghost_0.position.coordinates()[1]
1425                - (v.sinh()) * ghost_0.position.coordinates()[2],
1426            -((5.0 * PI / 4.0).sin()) * ghost_0.position.coordinates()[0]
1427                + ((5.0 * PI / 4.0).cos()) * ghost_0.position.coordinates()[1],
1428            (v.cosh()) * ghost_0.position.coordinates()[2]
1429                - (v.sinh()) * ((5.0 * PI / 4.0).cos()) * ghost_0.position.coordinates()[0]
1430                - (v.sinh()) * ((5.0 * PI / 4.0).sin()) * ghost_0.position.coordinates()[1],
1431        ]));
1432        let (angle_0, boost_0) = (
1433            ghost_0_center.coordinates()[1].atan2(ghost_0_center.coordinates()[0]),
1434            (ghost_0_center.coordinates()[2]).acosh(),
1435        );
1436        let (int_0, _o_0) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1437            9.0 * PI / 8.0 - angle_0,
1438            boost_0,
1439        );
1440        // check that intersection is minimum in center frame
1441        let int_c = Hyperbolic::<3>::from_polar_coordinates(int_0, 9.0 * PI / 8.0);
1442        let int_c_plus = Hyperbolic::<3>::from_polar_coordinates(int_0 + 0.05, 9.0 * PI / 8.0);
1443        let int_c_minus = Hyperbolic::<3>::from_polar_coordinates(int_0 - 0.05, 9.0 * PI / 8.0);
1444        assert!(ghost_0_center.distance(&int_c) < ghost_0_center.distance(&int_c_minus));
1445        assert!(ghost_0_center.distance(&int_c) < ghost_0_center.distance(&int_c_plus));
1446
1447        let intersection_0 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1448            (v.cosh()) * ((5.0 * PI / 4.0).cos()) * (int_0.sinh()) * ((9.0 * PI / 8.0).cos())
1449                - ((5.0 * PI / 4.0).sin()) * (int_0.sinh()) * ((9.0 * PI / 8.0).sin())
1450                + (v.sinh()) * ((5.0 * PI / 4.0).cos()) * (int_0.cosh()),
1451            (v.cosh()) * ((5.0 * PI / 4.0).sin()) * (int_0.sinh()) * ((9.0 * PI / 8.0).cos())
1452                + ((5.0 * PI / 4.0).cos()) * (int_0.sinh()) * ((9.0 * PI / 8.0).sin())
1453                + (v.sinh()) * ((5.0 * PI / 4.0).sin()) * (int_0.cosh()),
1454            (v.sinh()) * (int_0.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.cosh()) * (int_0.cosh()),
1455        ]));
1456
1457        // check that intersection point is also a minimum
1458        let blip = 0.05;
1459        let intersection_plus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1460            (v.cosh())
1461                * ((5.0 * PI / 4.0).cos())
1462                * ((int_0 + blip).sinh())
1463                * ((9.0 * PI / 8.0).cos())
1464                - ((5.0 * PI / 4.0).sin()) * ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).sin())
1465                + (v.sinh()) * ((5.0 * PI / 4.0).cos()) * ((int_0 + blip).cosh()),
1466            (v.cosh())
1467                * ((5.0 * PI / 4.0).sin())
1468                * ((int_0 + blip).sinh())
1469                * ((9.0 * PI / 8.0).cos())
1470                + ((5.0 * PI / 4.0).cos()) * ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).sin())
1471                + (v.sinh()) * ((5.0 * PI / 4.0).sin()) * ((int_0 + blip).cosh()),
1472            (v.sinh()) * ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).cos())
1473                + (v.cosh()) * ((int_0 + blip).cosh()),
1474        ]));
1475        let intersection_minus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1476            (v.cosh())
1477                * ((5.0 * PI / 4.0).cos())
1478                * ((int_0 - blip).sinh())
1479                * ((9.0 * PI / 8.0).cos())
1480                - ((5.0 * PI / 4.0).sin()) * ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).sin())
1481                + (v.sinh()) * ((5.0 * PI / 4.0).cos()) * ((int_0 - blip).cosh()),
1482            (v.cosh())
1483                * ((5.0 * PI / 4.0).sin())
1484                * ((int_0 - blip).sinh())
1485                * ((9.0 * PI / 8.0).cos())
1486                + ((5.0 * PI / 4.0).cos()) * ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).sin())
1487                + (v.sinh()) * ((5.0 * PI / 4.0).sin()) * ((int_0 - blip).cosh()),
1488            (v.sinh()) * ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).cos())
1489                + (v.cosh()) * ((int_0 - blip).cosh()),
1490        ]));
1491        assert!(
1492            ghost_0.position.distance(&intersection_0)
1493                < ghost_0.position.distance(&intersection_minus)
1494        );
1495        assert!(
1496            ghost_0.position.distance(&intersection_0)
1497                < ghost_0.position.distance(&intersection_plus)
1498        );
1499
1500        let int_to_ghost = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1501            &intersection_0,
1502            &ghost_0.position,
1503        );
1504        let v_to_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1505            &Hyperbolic::<3>::from_polar_coordinates(v, 5.0 * PI / 4.0),
1506            &intersection_0,
1507        );
1508        let tang_0 = 3.0 * PI / 8.0 + v_to_int;
1509        let ans_0 = (relative_orientation_upper + tang_0 + int_to_ghost).rem_euclid(2.0 * PI);
1510        assert_relative_eq!(
1511            ans_0.rem_euclid(2.0 * PI),
1512            ghost_0.orientation.theta.rem_euclid(2.0 * PI),
1513            epsilon = 1e-12
1514        );
1515
1516        // check remaining ghosts in less detail
1517        // ghost 1
1518        let ghost_1 = ghost_array[1];
1519        let ghost_1_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1520            (v.cosh()) * ((3.0 * PI / 4.0).cos()) * ghost_1.position.coordinates()[0]
1521                + (v.cosh()) * ((3.0 * PI / 4.0).sin()) * ghost_1.position.coordinates()[1]
1522                - (v.sinh()) * ghost_1.position.coordinates()[2],
1523            -((3.0 * PI / 4.0).sin()) * ghost_1.position.coordinates()[0]
1524                + ((3.0 * PI / 4.0).cos()) * ghost_1.position.coordinates()[1],
1525            (v.cosh()) * ghost_1.position.coordinates()[2]
1526                - (v.sinh()) * ((3.0 * PI / 4.0).cos()) * ghost_1.position.coordinates()[0]
1527                - (v.sinh()) * ((3.0 * PI / 4.0).sin()) * ghost_1.position.coordinates()[1],
1528        ]));
1529        let (angle_1, boost_1) = (
1530            ghost_1_center.coordinates()[1].atan2(ghost_1_center.coordinates()[0]),
1531            (ghost_1_center.coordinates()[2]).acosh(),
1532        );
1533        let (int_1, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1534            angle_1 - 7.0 * PI / 8.0,
1535            boost_1,
1536        );
1537        let intersection_1 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1538            (v.cosh()) * ((3.0 * PI / 4.0).cos()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos())
1539                - ((3.0 * PI / 4.0).sin()) * (int_1.sinh()) * ((7.0 * PI / 8.0).sin())
1540                + (v.sinh()) * ((3.0 * PI / 4.0).cos()) * (int_1.cosh()),
1541            (v.cosh()) * ((3.0 * PI / 4.0).sin()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos())
1542                + ((3.0 * PI / 4.0).cos()) * (int_1.sinh()) * ((7.0 * PI / 8.0).sin())
1543                + (v.sinh()) * ((3.0 * PI / 4.0).sin()) * (int_1.cosh()),
1544            (v.sinh()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_1.cosh()),
1545        ]));
1546        let int_to_ghost_1 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1547            &intersection_1,
1548            &ghost_1.position,
1549        );
1550        let v_to_int_1 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1551            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 4.0),
1552            &intersection_1,
1553        );
1554        let tang_1 = 13.0 * PI / 8.0 + v_to_int_1;
1555        let ans_1 = (relative_orientation_lower + tang_1 + int_to_ghost_1).rem_euclid(2.0 * PI);
1556        assert_relative_eq!(ans_1, ghost_1.orientation.theta, epsilon = 1e-12);
1557
1558        // ghost 2
1559        let ghost_2 = ghost_array[2];
1560        let ghost_2_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1561            (v.cosh()) * ((PI / 2.0).cos()) * ghost_2.position.coordinates()[0]
1562                + (v.cosh()) * ((PI / 2.0).sin()) * ghost_2.position.coordinates()[1]
1563                - (v.sinh()) * ghost_2.position.coordinates()[2],
1564            -((PI / 2.0).sin()) * ghost_2.position.coordinates()[0]
1565                + ((PI / 2.0).cos()) * ghost_2.position.coordinates()[1],
1566            (v.cosh()) * ghost_2.position.coordinates()[2]
1567                - (v.sinh()) * ((PI / 2.0).cos()) * ghost_2.position.coordinates()[0]
1568                - (v.sinh()) * ((PI / 2.0).sin()) * ghost_2.position.coordinates()[1],
1569        ]));
1570        let (angle_2, boost_2) = (
1571            ghost_2_center.coordinates()[1].atan2(ghost_2_center.coordinates()[0]),
1572            (ghost_2_center.coordinates()[2]).acosh(),
1573        );
1574        let (int_2, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1575            angle_2 - 11.0 * PI / 8.0,
1576            boost_2,
1577        );
1578        let intersection_2 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1579            (v.cosh()) * ((PI / 2.0).cos()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos())
1580                - ((PI / 2.0).sin()) * (int_2.sinh()) * ((11.0 * PI / 8.0).sin())
1581                + (v.sinh()) * ((PI / 2.0).cos()) * (int_2.cosh()),
1582            (v.cosh()) * ((PI / 2.0).sin()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos())
1583                + ((PI / 2.0).cos()) * (int_2.sinh()) * ((11.0 * PI / 8.0).sin())
1584                + (v.sinh()) * ((PI / 2.0).sin()) * (int_2.cosh()),
1585            (v.sinh()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos()) + (v.cosh()) * (int_2.cosh()),
1586        ]));
1587        let int_to_ghost_2 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1588            &intersection_2,
1589            &ghost_2.position,
1590        );
1591        let v_to_int_2 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1592            &Hyperbolic::<3>::from_polar_coordinates(v, PI / 2.0),
1593            &intersection_2,
1594        );
1595        let tang_2 = 15.0 * PI / 8.0 + v_to_int_2;
1596        let ans_2 = (relative_orientation_upper + tang_2 + int_to_ghost_2).rem_euclid(2.0 * PI);
1597        assert_relative_eq!(
1598            ans_2.rem_euclid(2.0 * PI),
1599            ghost_2.orientation.theta.rem_euclid(2.0 * PI),
1600            epsilon = 1e-12
1601        );
1602
1603        // ghost 3
1604        let ghost_3 = ghost_array[3];
1605        let ghost_3_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1606            (v.cosh()) * ((3.0 * PI / 2.0).cos()) * ghost_3.position.coordinates()[0]
1607                + (v.cosh()) * ((3.0 * PI / 2.0).sin()) * ghost_3.position.coordinates()[1]
1608                - (v.sinh()) * ghost_3.position.coordinates()[2],
1609            -((3.0 * PI / 2.0).sin()) * ghost_3.position.coordinates()[0]
1610                + ((3.0 * PI / 2.0).cos()) * ghost_3.position.coordinates()[1],
1611            (v.cosh()) * ghost_3.position.coordinates()[2]
1612                - (v.sinh()) * ((3.0 * PI / 2.0).cos()) * ghost_3.position.coordinates()[0]
1613                - (v.sinh()) * ((3.0 * PI / 2.0).sin()) * ghost_3.position.coordinates()[1],
1614        ]));
1615        let (angle_3, boost_3) = (
1616            ghost_3_center.coordinates()[1].atan2(ghost_3_center.coordinates()[0]),
1617            (ghost_3_center.coordinates()[2]).acosh(),
1618        );
1619        let (int_3, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1620            5.0 * PI / 8.0 - angle_3,
1621            boost_3,
1622        );
1623        let intersection_3 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1624            (v.cosh()) * ((3.0 * PI / 2.0).cos()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos())
1625                - ((3.0 * PI / 2.0).sin()) * (int_3.sinh()) * ((5.0 * PI / 8.0).sin())
1626                + (v.sinh()) * ((3.0 * PI / 2.0).cos()) * (int_3.cosh()),
1627            (v.cosh()) * ((3.0 * PI / 2.0).sin()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos())
1628                + ((3.0 * PI / 2.0).cos()) * (int_3.sinh()) * ((5.0 * PI / 8.0).sin())
1629                + (v.sinh()) * ((3.0 * PI / 2.0).sin()) * (int_3.cosh()),
1630            (v.sinh()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos()) + (v.cosh()) * (int_3.cosh()),
1631        ]));
1632        let int_to_ghost_3 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1633            &intersection_3,
1634            &ghost_3.position,
1635        );
1636        let v_to_int_3 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1637            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 2.0),
1638            &intersection_3,
1639        );
1640        let tang_3 = PI / 8.0 + v_to_int_3;
1641        let ans_3 = (relative_orientation_lower + tang_3 + int_to_ghost_3).rem_euclid(2.0 * PI);
1642        assert_relative_eq!(
1643            ans_3.rem_euclid(2.0 * PI),
1644            ghost_3.orientation.theta.rem_euclid(2.0 * PI),
1645            epsilon = 1e-12
1646        );
1647
1648        // ghost 4
1649        let ghost_4 = ghost_array[4];
1650        let ghost_4_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1651            (v.cosh()) * ((7.0 * PI / 4.0).cos()) * ghost_4.position.coordinates()[0]
1652                + (v.cosh()) * ((7.0 * PI / 4.0).sin()) * ghost_4.position.coordinates()[1]
1653                - (v.sinh()) * ghost_4.position.coordinates()[2],
1654            -((7.0 * PI / 4.0).sin()) * ghost_4.position.coordinates()[0]
1655                + ((7.0 * PI / 4.0).cos()) * ghost_4.position.coordinates()[1],
1656            (v.cosh()) * ghost_4.position.coordinates()[2]
1657                - (v.sinh()) * ((7.0 * PI / 4.0).cos()) * ghost_4.position.coordinates()[0]
1658                - (v.sinh()) * ((7.0 * PI / 4.0).sin()) * ghost_4.position.coordinates()[1],
1659        ]));
1660        let (angle_4, boost_4) = (
1661            ghost_4_center.coordinates()[1]
1662                .atan2(ghost_4_center.coordinates()[0])
1663                .rem_euclid(2.0 * PI),
1664            (ghost_4_center.coordinates()[2]).acosh(),
1665        );
1666        let (int_4, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1667            angle_4 - 13.0 * PI / 8.0,
1668            boost_4,
1669        );
1670        let intersection_4 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1671            (v.cosh()) * ((7.0 * PI / 4.0).cos()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos())
1672                - ((7.0 * PI / 4.0).sin()) * (int_4.sinh()) * ((13.0 * PI / 8.0).sin())
1673                + (v.sinh()) * ((7.0 * PI / 4.0).cos()) * (int_4.cosh()),
1674            (v.cosh()) * ((7.0 * PI / 4.0).sin()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos())
1675                + ((7.0 * PI / 4.0).cos()) * (int_4.sinh()) * ((13.0 * PI / 8.0).sin())
1676                + (v.sinh()) * ((7.0 * PI / 4.0).sin()) * (int_4.cosh()),
1677            (v.sinh()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos()) + (v.cosh()) * (int_4.cosh()),
1678        ]));
1679        let int_to_ghost_4 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1680            &intersection_4,
1681            &ghost_4.position,
1682        );
1683        let v_to_int_4 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1684            &Hyperbolic::<3>::from_polar_coordinates(v, 7.0 * PI / 4.0),
1685            &intersection_4,
1686        );
1687        let tang_4 = 11.0 * PI / 8.0 + v_to_int_4;
1688        let ans_4 = (relative_orientation_upper + tang_4 + int_to_ghost_4).rem_euclid(2.0 * PI);
1689        assert_relative_eq!(
1690            ans_4.rem_euclid(2.0 * PI),
1691            ghost_4.orientation.theta.rem_euclid(2.0 * PI),
1692            epsilon = 1e-12
1693        );
1694
1695        // ghost 5
1696        let ghost_5 = ghost_array[5];
1697        let ghost_5_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1698            (v.cosh()) * ((PI / 4.0).cos()) * ghost_5.position.coordinates()[0]
1699                + (v.cosh()) * ((PI / 4.0).sin()) * ghost_5.position.coordinates()[1]
1700                - (v.sinh()) * ghost_5.position.coordinates()[2],
1701            -((PI / 4.0).sin()) * ghost_5.position.coordinates()[0]
1702                + ((PI / 4.0).cos()) * ghost_5.position.coordinates()[1],
1703            (v.cosh()) * ghost_5.position.coordinates()[2]
1704                - (v.sinh()) * ((PI / 4.0).cos()) * ghost_5.position.coordinates()[0]
1705                - (v.sinh()) * ((PI / 4.0).sin()) * ghost_5.position.coordinates()[1],
1706        ]));
1707        let (angle_5, boost_5) = (
1708            ghost_5_center.coordinates()[1].atan2(ghost_5_center.coordinates()[0]),
1709            (ghost_5_center.coordinates()[2]).acosh(),
1710        );
1711        let (int_5, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1712            3.0 * PI / 8.0 - angle_5,
1713            boost_5,
1714        );
1715        let intersection_5 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1716            (v.cosh()) * ((PI / 4.0).cos()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos())
1717                - ((PI / 4.0).sin()) * (int_5.sinh()) * ((3.0 * PI / 8.0).sin())
1718                + (v.sinh()) * ((PI / 4.0).cos()) * (int_5.cosh()),
1719            (v.cosh()) * ((PI / 4.0).sin()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos())
1720                + ((PI / 4.0).cos()) * (int_5.sinh()) * ((3.0 * PI / 8.0).sin())
1721                + (v.sinh()) * ((PI / 4.0).sin()) * (int_5.cosh()),
1722            (v.sinh()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos()) + (v.cosh()) * (int_5.cosh()),
1723        ]));
1724        let int_to_ghost_5 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1725            &intersection_5,
1726            &ghost_5.position,
1727        );
1728        let v_to_int_5 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1729            &Hyperbolic::<3>::from_polar_coordinates(v, PI / 4.0),
1730            &intersection_5,
1731        );
1732        let tang_5 = 5.0 * PI / 8.0 + v_to_int_5;
1733        let ans_5 = (relative_orientation_lower + tang_5 + int_to_ghost_5).rem_euclid(2.0 * PI);
1734        assert_relative_eq!(
1735            ans_5.rem_euclid(2.0 * PI),
1736            ghost_5.orientation.theta.rem_euclid(2.0 * PI),
1737            epsilon = 1e-12
1738        );
1739
1740        // ghost 6
1741        let ghost_6 = ghost_array[6];
1742        let ghost_6_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1743            (v.cosh()) * ((PI).cos()) * ghost_6.position.coordinates()[0]
1744                + (v.cosh()) * ((PI).sin()) * ghost_6.position.coordinates()[1]
1745                - (v.sinh()) * ghost_6.position.coordinates()[2],
1746            -((PI).sin()) * ghost_6.position.coordinates()[0]
1747                + ((PI).cos()) * ghost_6.position.coordinates()[1],
1748            (v.cosh()) * ghost_6.position.coordinates()[2]
1749                - (v.sinh()) * ((PI).cos()) * ghost_6.position.coordinates()[0]
1750                - (v.sinh()) * ((PI).sin()) * ghost_6.position.coordinates()[1],
1751        ]));
1752        let (angle_6, boost_6) = (
1753            ghost_6_center.coordinates()[1]
1754                .atan2(ghost_6_center.coordinates()[0])
1755                .rem_euclid(2.0 * PI),
1756            (ghost_6_center.coordinates()[2]).acosh(),
1757        );
1758        let (int_6, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1759            angle_6 - 15.0 * PI / 8.0,
1760            boost_6,
1761        );
1762        let intersection_6 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1763            (v.cosh()) * ((PI).cos()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos())
1764                - ((PI).sin()) * (int_6.sinh()) * ((15.0 * PI / 8.0).sin())
1765                + (v.sinh()) * ((PI).cos()) * (int_6.cosh()),
1766            (v.cosh()) * ((PI).sin()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos())
1767                + ((PI).cos()) * (int_6.sinh()) * ((15.0 * PI / 8.0).sin())
1768                + (v.sinh()) * ((PI).sin()) * (int_6.cosh()),
1769            (v.sinh()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos()) + (v.cosh()) * (int_6.cosh()),
1770        ]));
1771        let int_to_ghost_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1772            &intersection_6,
1773            &ghost_6.position,
1774        );
1775        let v_to_int_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1776            &Hyperbolic::<3>::from_polar_coordinates(v, PI),
1777            &intersection_6,
1778        );
1779        let tang_6 = 7.0 * PI / 8.0 + v_to_int_6;
1780        let ans_6 = (relative_orientation_upper + tang_6 + int_to_ghost_6).rem_euclid(2.0 * PI);
1781        assert_relative_eq!(
1782            ans_6.rem_euclid(2.0 * PI),
1783            ghost_6.orientation.theta.rem_euclid(2.0 * PI),
1784            epsilon = 1e-12
1785        );
1786
1787        let (int_6_b, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1788            (PI / 8.0 - angle_6).rem_euclid(2.0 * PI),
1789            boost_6,
1790        );
1791        let intersection_6_b = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1792            (v.cosh()) * ((PI).cos()) * (int_6_b.sinh()) * ((PI / 8.0).cos())
1793                - ((PI).sin()) * (int_6_b.sinh()) * ((PI / 8.0).sin())
1794                + (v.sinh()) * ((PI).cos()) * (int_6_b.cosh()),
1795            (v.cosh()) * ((PI).sin()) * (int_6_b.sinh()) * ((PI / 8.0).cos())
1796                + ((PI).cos()) * (int_6_b.sinh()) * ((PI / 8.0).sin())
1797                + (v.sinh()) * ((PI).sin()) * (int_6_b.cosh()),
1798            (v.sinh()) * (int_6_b.sinh()) * ((PI / 8.0).cos()) + (v.cosh()) * (int_6_b.cosh()),
1799        ]));
1800        let int_to_ghost_6_b = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1801            &intersection_6_b,
1802            &ghost_6.position,
1803        );
1804        let v_to_int_6_b = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1805            &Hyperbolic::<3>::from_polar_coordinates(v, PI),
1806            &intersection_6_b,
1807        );
1808        let tang_6_b = 9.0 * PI / 8.0 + v_to_int_6_b;
1809        let ans_6_b =
1810            (relative_orientation_lower + tang_6_b + int_to_ghost_6_b).rem_euclid(2.0 * PI);
1811        assert_relative_eq!(
1812            ans_6_b.rem_euclid(2.0 * PI),
1813            ghost_6.orientation.theta.rem_euclid(2.0 * PI),
1814            epsilon = 1e-12
1815        );
1816    }
1817
1818    #[test]
1819    #[allow(clippy::too_many_lines, reason = "complicated function")]
1820    fn ghost_near_third_vertex_orientation() {
1821        let offset_boost = 0.25;
1822        let v: f64 = 2.448_452_447_678_076;
1823        let point = Hyperbolic::<3>::from_polar_coordinates(v - offset_boost, 3.0 * PI / 4.0 + 0.1);
1824        let point = OrientedHyperbolicPoint {
1825            position: point,
1826            orientation: Angle::from(PI),
1827        };
1828
1829        // get nearest boundary point
1830        let point_in_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1831            (v.cosh()) * ((3.0 * PI / 4.0).cos()) * point.position.coordinates()[0]
1832                + (v.cosh()) * ((3.0 * PI / 4.0).sin()) * point.position.coordinates()[1]
1833                - (v.sinh()) * point.position.coordinates()[2],
1834            -((3.0 * PI / 4.0).sin()) * point.position.coordinates()[0]
1835                + ((3.0 * PI / 4.0).cos()) * point.position.coordinates()[1],
1836            -(v.sinh()) * ((3.0 * PI / 4.0).cos()) * point.position.coordinates()[0]
1837                - (v.sinh()) * ((3.0 * PI / 4.0).sin()) * point.position.coordinates()[1]
1838                + (v.cosh()) * point.position.coordinates()[2],
1839        ]));
1840        let (angle_c, boost_c) = (
1841            point_in_center.coordinates()[1]
1842                .atan2(point_in_center.coordinates()[0])
1843                .rem_euclid(2.0 * PI),
1844            (point_in_center.coordinates()[2]).acosh(),
1845        );
1846        let (int_o, _o_o) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1847            angle_c - 7.0 * PI / 8.0,
1848            boost_c,
1849        );
1850        // have confirmed that this is the shortest path from the point to the boundary
1851        let intersection_o = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1852            (v.cosh()) * ((3.0 * PI / 4.0).cos()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos())
1853                - ((3.0 * PI / 4.0).sin()) * (int_o.sinh()) * ((7.0 * PI / 8.0).sin())
1854                + (v.sinh()) * ((3.0 * PI / 4.0).cos()) * (int_o.cosh()),
1855            (v.cosh()) * ((3.0 * PI / 4.0).sin()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos())
1856                + ((3.0 * PI / 4.0).cos()) * (int_o.sinh()) * ((7.0 * PI / 8.0).sin())
1857                + (v.sinh()) * ((3.0 * PI / 4.0).sin()) * (int_o.cosh()),
1858            (v.sinh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_o.cosh()),
1859        ]));
1860
1861        let int_to_v_o = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1862            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 4.0),
1863            &intersection_o,
1864        );
1865        let tang_o = 13.0 * PI / 8.0 + int_to_v_o;
1866        let relative_orientation_upper = PI
1867            + (OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1868                &point.position,
1869                &intersection_o,
1870            ) - tang_o)
1871                .rem_euclid(2.0 * PI);
1872
1873        let (int_ol, _o_ol) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1874            9.0 * PI / 8.0 - angle_c,
1875            boost_c,
1876        );
1877
1878        let intersection_ol = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1879            (v.cosh()) * ((3.0 * PI / 4.0).cos()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).cos())
1880                - ((3.0 * PI / 4.0).sin()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).sin())
1881                + (v.sinh()) * ((3.0 * PI / 4.0).cos()) * (int_ol.cosh()),
1882            (v.cosh()) * ((3.0 * PI / 4.0).sin()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).cos())
1883                + ((3.0 * PI / 4.0).cos()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).sin())
1884                + (v.sinh()) * ((3.0 * PI / 4.0).sin()) * (int_ol.cosh()),
1885            (v.sinh()) * (int_ol.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.cosh()) * (int_ol.cosh()),
1886        ]));
1887
1888        let int_to_v_ol = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1889            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 4.0),
1890            &intersection_ol,
1891        );
1892        let tang_ol = 15.0 * PI / 8.0 + int_to_v_ol;
1893        let relative_orientation_lower = PI
1894            + (OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1895                &point.position,
1896                &intersection_ol,
1897            ) - tang_ol)
1898                .rem_euclid(2.0 * PI);
1899
1900        let periodic = Periodic::new(0.5, EightEight {}).expect("hard-coded positive number");
1901
1902        let ghost_array = periodic.generate_ghosts(&point);
1903
1904        let ghost_0 = ghost_array[0];
1905        let ghost_0_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1906            (v.cosh()) * ghost_0.position.coordinates()[0]
1907                - (v.sinh()) * ghost_0.position.coordinates()[2],
1908            ghost_0.position.coordinates()[1],
1909            (v.cosh()) * ghost_0.position.coordinates()[2]
1910                - (v.sinh()) * ghost_0.position.coordinates()[0],
1911        ]));
1912        let (angle_0, boost_0) = (
1913            ghost_0_center.coordinates()[1].atan2(ghost_0_center.coordinates()[0]),
1914            (ghost_0_center.coordinates()[2]).acosh(),
1915        );
1916        let (int_0, _o_0) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1917            angle_0 - 9.0 * PI / 8.0,
1918            boost_0,
1919        );
1920        // check that intersection is minimum in center frame
1921        let int_c = Hyperbolic::<3>::from_polar_coordinates(int_0, 9.0 * PI / 8.0);
1922        let int_c_plus = Hyperbolic::<3>::from_polar_coordinates(int_0 + 0.05, 9.0 * PI / 8.0);
1923        let int_c_minus = Hyperbolic::<3>::from_polar_coordinates(int_0 - 0.05, 9.0 * PI / 8.0);
1924        assert!(ghost_0_center.distance(&int_c) < ghost_0_center.distance(&int_c_minus));
1925        assert!(ghost_0_center.distance(&int_c) < ghost_0_center.distance(&int_c_plus));
1926
1927        let intersection_0 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1928            (v.cosh()) * (int_0.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.sinh()) * (int_0.cosh()),
1929            (int_0.sinh()) * ((9.0 * PI / 8.0).sin()),
1930            (v.sinh()) * (int_0.sinh()) * ((9.0 * PI / 8.0).cos()) + (v.cosh()) * (int_0.cosh()),
1931        ]));
1932
1933        // check that intersection point is also a minimum
1934        let blip = 0.05;
1935        let intersection_plus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1936            (v.cosh()) * ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).cos())
1937                + (v.sinh()) * ((int_0 + blip).cosh()),
1938            ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).sin()),
1939            (v.sinh()) * ((int_0 + blip).sinh()) * ((9.0 * PI / 8.0).cos())
1940                + (v.cosh()) * ((int_0 + blip).cosh()),
1941        ]));
1942        let intersection_minus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1943            (v.cosh()) * ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).cos())
1944                + (v.sinh()) * ((int_0 - blip).cosh()),
1945            ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).sin()),
1946            (v.sinh()) * ((int_0 - blip).sinh()) * ((9.0 * PI / 8.0).cos())
1947                + (v.cosh()) * ((int_0 - blip).cosh()),
1948        ]));
1949        assert!(
1950            ghost_0.position.distance(&intersection_0)
1951                < ghost_0.position.distance(&intersection_minus)
1952        );
1953        assert!(
1954            ghost_0.position.distance(&intersection_0)
1955                < ghost_0.position.distance(&intersection_plus)
1956        );
1957
1958        let int_to_ghost = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1959            &intersection_0,
1960            &ghost_0.position,
1961        );
1962        let v_to_int = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
1963            &Hyperbolic::<3>::from_polar_coordinates(v, 0.0),
1964            &intersection_0,
1965        );
1966        let tang_0 = 9.0 * PI / 8.0 + v_to_int;
1967        let ans_0 = (relative_orientation_upper + tang_0 + int_to_ghost).rem_euclid(2.0 * PI);
1968        assert_relative_eq!(ans_0, ghost_0.orientation.theta, epsilon = 1e-12);
1969
1970        // check remaining ghosts in less detail
1971        // ghost 1
1972        let ghost_1 = ghost_array[1];
1973        let ghost_1_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1974            (v.cosh()) * ((3.0 * PI / 2.0).cos()) * ghost_1.position.coordinates()[0]
1975                + (v.cosh()) * ((3.0 * PI / 2.0).sin()) * ghost_1.position.coordinates()[1]
1976                - (v.sinh()) * ghost_1.position.coordinates()[2],
1977            -((3.0 * PI / 2.0).sin()) * ghost_1.position.coordinates()[0]
1978                + ((3.0 * PI / 2.0).cos()) * ghost_1.position.coordinates()[1],
1979            (v.cosh()) * ghost_1.position.coordinates()[2]
1980                - (v.sinh()) * ((3.0 * PI / 2.0).cos()) * ghost_1.position.coordinates()[0]
1981                - (v.sinh()) * ((3.0 * PI / 2.0).sin()) * ghost_1.position.coordinates()[1],
1982        ]));
1983        let (angle_1, boost_1) = (
1984            ghost_1_center.coordinates()[1].atan2(ghost_1_center.coordinates()[0]),
1985            (ghost_1_center.coordinates()[2]).acosh(),
1986        );
1987        let (int_1, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
1988            7.0 * PI / 8.0 - angle_1,
1989            boost_1,
1990        );
1991        let intersection_1 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
1992            (v.cosh()) * ((3.0 * PI / 2.0).cos()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos())
1993                - ((3.0 * PI / 2.0).sin()) * (int_1.sinh()) * ((7.0 * PI / 8.0).sin())
1994                + (v.sinh()) * ((3.0 * PI / 2.0).cos()) * (int_1.cosh()),
1995            (v.cosh()) * ((3.0 * PI / 2.0).sin()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos())
1996                + ((3.0 * PI / 2.0).cos()) * (int_1.sinh()) * ((7.0 * PI / 8.0).sin())
1997                + (v.sinh()) * ((3.0 * PI / 2.0).sin()) * (int_1.cosh()),
1998            (v.sinh()) * (int_1.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_1.cosh()),
1999        ]));
2000        let int_to_ghost_1 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2001            &intersection_1,
2002            &ghost_1.position,
2003        );
2004        let v_to_int_1 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2005            &Hyperbolic::<3>::from_polar_coordinates(v, 3.0 * PI / 2.0),
2006            &intersection_1,
2007        );
2008        let tang_1 = 3.0 * PI / 8.0 + v_to_int_1;
2009        let ans_1 = (relative_orientation_lower + tang_1 + int_to_ghost_1).rem_euclid(2.0 * PI);
2010        assert_relative_eq!(
2011            ans_1.rem_euclid(2.0 * PI),
2012            ghost_1.orientation.theta.rem_euclid(2.0 * PI),
2013            epsilon = 1e-12
2014        );
2015
2016        // ghost 2
2017        let ghost_2 = ghost_array[2];
2018        let ghost_2_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2019            (v.cosh()) * ((5.0 * PI / 4.0).cos()) * ghost_2.position.coordinates()[0]
2020                + (v.cosh()) * ((5.0 * PI / 4.0).sin()) * ghost_2.position.coordinates()[1]
2021                - (v.sinh()) * ghost_2.position.coordinates()[2],
2022            -((5.0 * PI / 4.0).sin()) * ghost_2.position.coordinates()[0]
2023                + ((5.0 * PI / 4.0).cos()) * ghost_2.position.coordinates()[1],
2024            (v.cosh()) * ghost_2.position.coordinates()[2]
2025                - (v.sinh()) * ((5.0 * PI / 4.0).cos()) * ghost_2.position.coordinates()[0]
2026                - (v.sinh()) * ((5.0 * PI / 4.0).sin()) * ghost_2.position.coordinates()[1],
2027        ]));
2028        let (angle_2, boost_2) = (
2029            ghost_2_center.coordinates()[1].atan2(ghost_2_center.coordinates()[0]),
2030            (ghost_2_center.coordinates()[2]).acosh(),
2031        );
2032        let (int_2, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2033            angle_2 - 11.0 * PI / 8.0,
2034            boost_2,
2035        );
2036        let intersection_2 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2037            (v.cosh()) * ((5.0 * PI / 4.0).cos()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos())
2038                - ((5.0 * PI / 4.0).sin()) * (int_2.sinh()) * ((11.0 * PI / 8.0).sin())
2039                + (v.sinh()) * ((5.0 * PI / 4.0).cos()) * (int_2.cosh()),
2040            (v.cosh()) * ((5.0 * PI / 4.0).sin()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos())
2041                + ((5.0 * PI / 4.0).cos()) * (int_2.sinh()) * ((11.0 * PI / 8.0).sin())
2042                + (v.sinh()) * ((5.0 * PI / 4.0).sin()) * (int_2.cosh()),
2043            (v.sinh()) * (int_2.sinh()) * ((11.0 * PI / 8.0).cos()) + (v.cosh()) * (int_2.cosh()),
2044        ]));
2045        let int_to_ghost_2 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2046            &intersection_2,
2047            &ghost_2.position,
2048        );
2049        let v_to_int_2 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2050            &Hyperbolic::<3>::from_polar_coordinates(v, 5.0 * PI / 4.0),
2051            &intersection_2,
2052        );
2053        let tang_2 = 5.0 * PI / 8.0 + v_to_int_2;
2054        let ans_2 = (relative_orientation_upper + tang_2 + int_to_ghost_2).rem_euclid(2.0 * PI);
2055        assert_relative_eq!(
2056            ans_2.rem_euclid(2.0 * PI),
2057            ghost_2.orientation.theta.rem_euclid(2.0 * PI),
2058            epsilon = 1e-12
2059        );
2060
2061        // ghost 3
2062        let ghost_3 = ghost_array[3];
2063        let ghost_3_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2064            (v.cosh()) * ((PI / 4.0).cos()) * ghost_3.position.coordinates()[0]
2065                + (v.cosh()) * ((PI / 4.0).sin()) * ghost_3.position.coordinates()[1]
2066                - (v.sinh()) * ghost_3.position.coordinates()[2],
2067            -((PI / 4.0).sin()) * ghost_3.position.coordinates()[0]
2068                + ((PI / 4.0).cos()) * ghost_3.position.coordinates()[1],
2069            (v.cosh()) * ghost_3.position.coordinates()[2]
2070                - (v.sinh()) * ((PI / 4.0).cos()) * ghost_3.position.coordinates()[0]
2071                - (v.sinh()) * ((PI / 4.0).sin()) * ghost_3.position.coordinates()[1],
2072        ]));
2073        let (angle_3, boost_3) = (
2074            ghost_3_center.coordinates()[1].atan2(ghost_3_center.coordinates()[0]),
2075            (ghost_3_center.coordinates()[2]).acosh(),
2076        );
2077        let (int_3, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2078            5.0 * PI / 8.0 - angle_3,
2079            boost_3,
2080        );
2081        let intersection_3 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2082            (v.cosh()) * ((PI / 4.0).cos()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos())
2083                - ((PI / 4.0).sin()) * (int_3.sinh()) * ((5.0 * PI / 8.0).sin())
2084                + (v.sinh()) * ((PI / 4.0).cos()) * (int_3.cosh()),
2085            (v.cosh()) * ((PI / 4.0).sin()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos())
2086                + ((PI / 4.0).cos()) * (int_3.sinh()) * ((5.0 * PI / 8.0).sin())
2087                + (v.sinh()) * ((PI / 4.0).sin()) * (int_3.cosh()),
2088            (v.sinh()) * (int_3.sinh()) * ((5.0 * PI / 8.0).cos()) + (v.cosh()) * (int_3.cosh()),
2089        ]));
2090        let int_to_ghost_3 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2091            &intersection_3,
2092            &ghost_3.position,
2093        );
2094        let v_to_int_3 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2095            &Hyperbolic::<3>::from_polar_coordinates(v, PI / 4.0),
2096            &intersection_3,
2097        );
2098        let tang_3 = 7.0 * PI / 8.0 + v_to_int_3;
2099        let ans_3 = (relative_orientation_lower + tang_3 + int_to_ghost_3).rem_euclid(2.0 * PI);
2100        assert_relative_eq!(
2101            ans_3.rem_euclid(2.0 * PI),
2102            ghost_3.orientation.theta.rem_euclid(2.0 * PI),
2103            epsilon = 1e-12
2104        );
2105
2106        // ghost 4
2107        let ghost_4 = ghost_array[4];
2108        let ghost_4_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2109            (v.cosh()) * ((PI / 2.0).cos()) * ghost_4.position.coordinates()[0]
2110                + (v.cosh()) * ((PI / 2.0).sin()) * ghost_4.position.coordinates()[1]
2111                - (v.sinh()) * ghost_4.position.coordinates()[2],
2112            -((PI / 2.0).sin()) * ghost_4.position.coordinates()[0]
2113                + ((PI / 2.0).cos()) * ghost_4.position.coordinates()[1],
2114            (v.cosh()) * ghost_4.position.coordinates()[2]
2115                - (v.sinh()) * ((PI / 2.0).cos()) * ghost_4.position.coordinates()[0]
2116                - (v.sinh()) * ((PI / 2.0).sin()) * ghost_4.position.coordinates()[1],
2117        ]));
2118        let (angle_4, boost_4) = (
2119            ghost_4_center.coordinates()[1].atan2(ghost_4_center.coordinates()[0]),
2120            (ghost_4_center.coordinates()[2]).acosh(),
2121        );
2122        let (int_4, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2123            angle_4 - 13.0 * PI / 8.0,
2124            boost_4,
2125        );
2126        let intersection_4 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2127            (v.cosh()) * ((PI / 2.0).cos()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos())
2128                - ((PI / 2.0).sin()) * (int_4.sinh()) * ((13.0 * PI / 8.0).sin())
2129                + (v.sinh()) * ((PI / 2.0).cos()) * (int_4.cosh()),
2130            (v.cosh()) * ((PI / 2.0).sin()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos())
2131                + ((PI / 2.0).cos()) * (int_4.sinh()) * ((13.0 * PI / 8.0).sin())
2132                + (v.sinh()) * ((PI / 2.0).sin()) * (int_4.cosh()),
2133            (v.sinh()) * (int_4.sinh()) * ((13.0 * PI / 8.0).cos()) + (v.cosh()) * (int_4.cosh()),
2134        ]));
2135        let int_to_ghost_4 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2136            &intersection_4,
2137            &ghost_4.position,
2138        );
2139        let v_to_int_4 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2140            &Hyperbolic::<3>::from_polar_coordinates(v, PI / 2.0),
2141            &intersection_4,
2142        );
2143        let tang_4 = PI / 8.0 + v_to_int_4;
2144        let ans_4 = (relative_orientation_upper + tang_4 + int_to_ghost_4).rem_euclid(2.0 * PI);
2145        assert_relative_eq!(
2146            ans_4.rem_euclid(2.0 * PI),
2147            ghost_4.orientation.theta.rem_euclid(2.0 * PI),
2148            epsilon = 1e-12
2149        );
2150
2151        // ghost 5
2152        let ghost_5 = ghost_array[5];
2153        let ghost_5_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2154            (v.cosh()) * ((PI).cos()) * ghost_5.position.coordinates()[0]
2155                + (v.cosh()) * ((PI).sin()) * ghost_5.position.coordinates()[1]
2156                - (v.sinh()) * ghost_5.position.coordinates()[2],
2157            -((PI).sin()) * ghost_5.position.coordinates()[0]
2158                + ((PI).cos()) * ghost_5.position.coordinates()[1],
2159            (v.cosh()) * ghost_5.position.coordinates()[2]
2160                - (v.sinh()) * ((PI).cos()) * ghost_5.position.coordinates()[0]
2161                - (v.sinh()) * ((PI).sin()) * ghost_5.position.coordinates()[1],
2162        ]));
2163        let (angle_5, boost_5) = (
2164            ghost_5_center.coordinates()[1].atan2(ghost_5_center.coordinates()[0]),
2165            (ghost_5_center.coordinates()[2]).acosh(),
2166        );
2167        let (int_5, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2168            3.0 * PI / 8.0 - angle_5,
2169            boost_5,
2170        );
2171        let intersection_5 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2172            (v.cosh()) * ((PI).cos()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos())
2173                - ((PI).sin()) * (int_5.sinh()) * ((3.0 * PI / 8.0).sin())
2174                + (v.sinh()) * ((PI).cos()) * (int_5.cosh()),
2175            (v.cosh()) * ((PI).sin()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos())
2176                + ((PI).cos()) * (int_5.sinh()) * ((3.0 * PI / 8.0).sin())
2177                + (v.sinh()) * ((PI).sin()) * (int_5.cosh()),
2178            (v.sinh()) * (int_5.sinh()) * ((3.0 * PI / 8.0).cos()) + (v.cosh()) * (int_5.cosh()),
2179        ]));
2180        let int_to_ghost_5 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2181            &intersection_5,
2182            &ghost_5.position,
2183        );
2184        let v_to_int_5 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2185            &Hyperbolic::<3>::from_polar_coordinates(v, PI),
2186            &intersection_5,
2187        );
2188        let tang_5 = 11.0 * PI / 8.0 + v_to_int_5;
2189        let ans_5 = (relative_orientation_lower + tang_5 + int_to_ghost_5).rem_euclid(2.0 * PI);
2190        assert_relative_eq!(
2191            ans_5.rem_euclid(2.0 * PI),
2192            ghost_5.orientation.theta.rem_euclid(2.0 * PI),
2193            epsilon = 1e-12
2194        );
2195
2196        // ghost 6
2197        let ghost_6 = ghost_array[6];
2198        let ghost_6_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2199            (v.cosh()) * ((7.0 * PI / 4.0).cos()) * ghost_6.position.coordinates()[0]
2200                + (v.cosh()) * ((7.0 * PI / 4.0).sin()) * ghost_6.position.coordinates()[1]
2201                - (v.sinh()) * ghost_6.position.coordinates()[2],
2202            -((7.0 * PI / 4.0).sin()) * ghost_6.position.coordinates()[0]
2203                + ((7.0 * PI / 4.0).cos()) * ghost_6.position.coordinates()[1],
2204            (v.cosh()) * ghost_6.position.coordinates()[2]
2205                - (v.sinh()) * ((7.0 * PI / 4.0).cos()) * ghost_6.position.coordinates()[0]
2206                - (v.sinh()) * ((7.0 * PI / 4.0).sin()) * ghost_6.position.coordinates()[1],
2207        ]));
2208        let (angle_6, boost_6) = (
2209            ghost_6_center.coordinates()[1].atan2(ghost_6_center.coordinates()[0]),
2210            (ghost_6_center.coordinates()[2]).acosh(),
2211        );
2212        let (int_6, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2213            15.0 * PI / 8.0 - angle_6,
2214            boost_6,
2215        );
2216        let intersection_6 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2217            (v.cosh()) * ((7.0 * PI / 4.0).cos()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos())
2218                - ((7.0 * PI / 4.0).sin()) * (int_6.sinh()) * ((15.0 * PI / 8.0).sin())
2219                + (v.sinh()) * ((7.0 * PI / 4.0).cos()) * (int_6.cosh()),
2220            (v.cosh()) * ((7.0 * PI / 4.0).sin()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos())
2221                + ((7.0 * PI / 4.0).cos()) * (int_6.sinh()) * ((15.0 * PI / 8.0).sin())
2222                + (v.sinh()) * ((7.0 * PI / 4.0).sin()) * (int_6.cosh()),
2223            (v.sinh()) * (int_6.sinh()) * ((15.0 * PI / 8.0).cos()) + (v.cosh()) * (int_6.cosh()),
2224        ]));
2225        let int_to_ghost_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2226            &intersection_6,
2227            &ghost_6.position,
2228        );
2229        let v_to_int_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2230            &Hyperbolic::<3>::from_polar_coordinates(v, 7.0 * PI / 4.0),
2231            &intersection_6,
2232        );
2233        let tang_6 = 13.0 * PI / 8.0 + v_to_int_6;
2234        let ans_6 = (relative_orientation_upper + tang_6 + int_to_ghost_6).rem_euclid(2.0 * PI);
2235        assert_relative_eq!(
2236            ans_6.rem_euclid(2.0 * PI),
2237            ghost_6.orientation.theta.rem_euclid(2.0 * PI),
2238            epsilon = 1e-12
2239        );
2240
2241        let (int_6, _o_1) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
2242            (PI / 8.0 - angle_6).rem_euclid(2.0 * PI),
2243            boost_6,
2244        );
2245        let intersection_6 = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
2246            (v.cosh()) * ((7.0 * PI / 4.0).cos()) * (int_6.sinh()) * ((PI / 8.0).cos())
2247                - ((7.0 * PI / 4.0).sin()) * (int_6.sinh()) * ((PI / 8.0).sin())
2248                + (v.sinh()) * ((7.0 * PI / 4.0).cos()) * (int_6.cosh()),
2249            (v.cosh()) * ((7.0 * PI / 4.0).sin()) * (int_6.sinh()) * ((PI / 8.0).cos())
2250                + ((7.0 * PI / 4.0).cos()) * (int_6.sinh()) * ((PI / 8.0).sin())
2251                + (v.sinh()) * ((7.0 * PI / 4.0).sin()) * (int_6.cosh()),
2252            (v.sinh()) * (int_6.sinh()) * ((PI / 8.0).cos()) + (v.cosh()) * (int_6.cosh()),
2253        ]));
2254        let int_to_ghost_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2255            &intersection_6,
2256            &ghost_6.position,
2257        );
2258        let v_to_int_6 = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(
2259            &Hyperbolic::<3>::from_polar_coordinates(v, 7.0 * PI / 4.0),
2260            &intersection_6,
2261        );
2262        let tang_6 = 15.0 * PI / 8.0 + v_to_int_6;
2263        let ans_6 = (relative_orientation_lower + tang_6 + int_to_ghost_6).rem_euclid(2.0 * PI);
2264        assert_relative_eq!(
2265            ans_6.rem_euclid(2.0 * PI),
2266            ghost_6.orientation.theta.rem_euclid(2.0 * PI),
2267            epsilon = 1e-12
2268        );
2269    }
2270}