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