1use std::borrow::Cow;
5use std::f32::consts::{PI, TAU};
6
7use glam::{BVec3, Mat4, Quat, Vec3, Vec4};
8
9use crate::euclidean::Iso3;
10use crate::space::{IsometryGroup, Space, WgslAccuracy, WgslSpace, FLAT_CHART_MAX_ARC};
11use crate::spherical::Iso4;
12use crate::spherical_embedded::SphericalS3Embedded;
13
14pub trait CoveringSpace: Space {
15 type CoverIso: Copy + Send + Sync + 'static;
16
17 fn cover_identity(&self) -> Self::CoverIso;
18
19 fn cover_compose(&self, a: Self::CoverIso, b: Self::CoverIso) -> Self::CoverIso;
20
21 fn cover_inverse(&self, a: Self::CoverIso) -> Self::CoverIso;
22
23 fn cover_apply(&self, iso: Self::CoverIso, p: Self::Point) -> Self::Point;
24
25 fn cover_transport(
26 &self,
27 iso: Self::CoverIso,
28 at: Self::Point,
29 v: Self::Vector,
30 ) -> Self::Vector;
31}
32
33pub trait QuotientSpace: CoveringSpace {
34 fn face_pairings(&self) -> impl Iterator<Item = Self::CoverIso>;
36
37 fn in_fundamental_domain(&self, p: Self::Point) -> bool;
38
39 fn wrap_to_domain(&self, p: Self::Point) -> (Self::Point, Self::CoverIso);
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct FlatTorus3 {
46 cell: Vec3,
47}
48
49impl FlatTorus3 {
50 pub fn new(cell: Vec3) -> Self {
52 assert!(
53 cell.is_finite() && cell.cmpgt(Vec3::ZERO).all(),
54 "FlatTorus3 cell must be finite and positive, got {cell:?}"
55 );
56 Self { cell }
57 }
58
59 pub fn cube(side: f32) -> Self {
60 Self::new(Vec3::splat(side))
61 }
62
63 pub fn cell(&self) -> Vec3 {
64 self.cell
65 }
66
67 pub fn injectivity_radius(&self) -> f32 {
68 0.5 * self.cell.min_element()
69 }
70
71 fn lattice_index(&self, p: Vec3) -> Vec3 {
72 let index = (p / self.cell + Vec3::splat(0.5)).floor();
73 let residual = p - self.cell * index;
74 let half = 0.5 * self.cell;
75 index - unit_where(residual.cmplt(-half)) + unit_where(residual.cmpge(half))
76 }
77
78 fn wrap(&self, p: Vec3) -> Vec3 {
79 p - self.cell * self.lattice_index(p)
80 }
81}
82
83fn unit_where(mask: BVec3) -> Vec3 {
84 Vec3::select(mask, Vec3::ONE, Vec3::ZERO)
85}
86
87impl Space for FlatTorus3 {
88 type Point = Vec3;
89 type Vector = Vec3;
90 type Frame = Quat;
91
92 fn frame_at(&self, _at: Vec3) -> Quat {
93 Quat::IDENTITY
94 }
95
96 fn distance(&self, a: Vec3, b: Vec3) -> f32 {
97 self.wrap(a - b).length()
98 }
99
100 fn exp(&self, at: Vec3, v: Vec3) -> Vec3 {
101 self.wrap(at + v)
102 }
103
104 fn log(&self, from: Vec3, to: Vec3) -> Vec3 {
105 self.wrap(to - from)
106 }
107
108 fn parallel_transport(&self, _from: Vec3, _to: Vec3, v: Vec3) -> Vec3 {
109 v
110 }
111
112 fn is_chart_flat(&self) -> bool {
113 false
114 }
115
116 fn chart_envelope(&self) -> f32 {
117 f32::INFINITY
118 }
119
120 fn valid_point(&self, p: Vec3) -> bool {
121 p.is_finite()
122 }
123}
124
125impl CoveringSpace for FlatTorus3 {
126 type CoverIso = Iso3;
127
128 fn cover_identity(&self) -> Iso3 {
129 Iso3::IDENTITY
130 }
131
132 fn cover_compose(&self, a: Iso3, b: Iso3) -> Iso3 {
133 Iso3 {
134 rotation: a.rotation * b.rotation,
135 translation: a.rotation * b.translation + a.translation,
136 }
137 }
138
139 fn cover_inverse(&self, a: Iso3) -> Iso3 {
140 let inverse_rotation = a.rotation.inverse();
141 Iso3 {
142 rotation: inverse_rotation,
143 translation: inverse_rotation * (-a.translation),
144 }
145 }
146
147 fn cover_apply(&self, iso: Iso3, p: Vec3) -> Vec3 {
148 iso.rotation * p + iso.translation
149 }
150
151 fn cover_transport(&self, iso: Iso3, _at: Vec3, v: Vec3) -> Vec3 {
152 iso.rotation * v
153 }
154}
155
156impl QuotientSpace for FlatTorus3 {
157 fn face_pairings(&self) -> impl Iterator<Item = Iso3> {
158 [
159 Iso3::from_translation(Vec3::new(self.cell.x, 0.0, 0.0)),
160 Iso3::from_translation(Vec3::new(0.0, self.cell.y, 0.0)),
161 Iso3::from_translation(Vec3::new(0.0, 0.0, self.cell.z)),
162 ]
163 .into_iter()
164 }
165
166 fn in_fundamental_domain(&self, p: Vec3) -> bool {
167 let half = 0.5 * self.cell;
168 p.cmpge(-half).all() && p.cmplt(half).all()
169 }
170
171 fn wrap_to_domain(&self, p: Vec3) -> (Vec3, Iso3) {
172 let translation = -(self.cell * self.lattice_index(p));
173
174 (p + translation, Iso3::from_translation(translation))
175 }
176}
177
178impl WgslSpace for FlatTorus3 {
179 fn wgsl_impl(&self) -> Cow<'static, str> {
180 Cow::Owned(flat_torus3_wgsl(self.cell))
181 }
182
183 fn wgsl_accuracy(&self) -> WgslAccuracy {
184 WgslAccuracy::Exact
185 }
186}
187
188fn flat_torus3_wgsl(cell: Vec3) -> String {
189 let (cx, cy, cz) = (cell.x, cell.y, cell.z);
190 let (hx, hy, hz) = (0.5 * cell.x, 0.5 * cell.y, 0.5 * cell.z);
191 format!(
192 r#"
193
194const LOAM_MAX_ARC: f32 = {max_arc:?};
195const LOAM_TORUS_CELL: vec3<f32> = vec3<f32>({cx:?}, {cy:?}, {cz:?});
196const LOAM_TORUS_HALF: vec3<f32> = vec3<f32>({hx:?}, {hy:?}, {hz:?});
197
198fn loam_torus_wrap(p: vec3<f32>) -> vec3<f32> {{
199 let index = floor(p / LOAM_TORUS_CELL + vec3<f32>(0.5));
200 let residual = p - LOAM_TORUS_CELL * index;
201 let below = select(vec3<f32>(0.0), LOAM_TORUS_CELL, residual < -LOAM_TORUS_HALF);
202 let above = select(vec3<f32>(0.0), LOAM_TORUS_CELL, residual >= LOAM_TORUS_HALF);
203 return residual + below - above;
204}}
205
206fn loam_distance(a: vec3<f32>, b: vec3<f32>) -> f32 {{ return length(loam_torus_wrap(a - b)); }}
207fn loam_origin_distance(p: vec3<f32>) -> f32 {{ return length(loam_torus_wrap(p)); }}
208fn loam_exp(at: vec3<f32>, v: vec3<f32>) -> vec3<f32> {{ return loam_torus_wrap(at + v); }}
209fn loam_log(p_from: vec3<f32>, p_to: vec3<f32>) -> vec3<f32> {{ return loam_torus_wrap(p_to - p_from); }}
210struct LoamGeodesicStep {{ p: vec3<f32>, v: vec3<f32> }}
211fn loam_geodesic_step(p: vec3<f32>, v: vec3<f32>, s: f32) -> LoamGeodesicStep {{
212 return LoamGeodesicStep(loam_exp(p, v * s), v);
213}}
214"#,
215 max_arc = FLAT_CHART_MAX_ARC
216 )
217}
218
219const COVER: SphericalS3Embedded = SphericalS3Embedded;
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub struct LensSpace {
226 p: u32,
227 q: u32,
228}
229
230impl LensSpace {
231 pub fn new(p: u32, q: u32) -> Self {
233 assert!(
234 p >= 2 && p <= i32::MAX as u32,
235 "LensSpace needs p >= 2 and p <= i32::MAX, got p = {p}"
236 );
237 let q = q % p;
238 assert!(
239 gcd(p, q) == 1,
240 "LensSpace needs gcd(p, q) = 1 for a free action, got p = {p}, q = {q}"
241 );
242 Self { p, q }
243 }
244
245 pub fn p(&self) -> u32 {
246 self.p
247 }
248
249 pub fn q(&self) -> u32 {
251 self.q
252 }
253
254 pub fn injectivity_radius(&self) -> f32 {
255 PI / self.p as f32
256 }
257
258 pub fn deck(&self, power: i32) -> Iso4 {
260 let (beta1, beta2) = self.deck_angles(power);
261 let (sin1, cos1) = beta1.sin_cos();
262 let (sin2, cos2) = beta2.sin_cos();
263 Iso4 {
264 matrix: Mat4::from_cols(
265 Vec4::new(cos1, sin1, 0.0, 0.0),
266 Vec4::new(-sin1, cos1, 0.0, 0.0),
267 Vec4::new(0.0, 0.0, cos2, sin2),
268 Vec4::new(0.0, 0.0, -sin2, cos2),
269 ),
270 }
271 }
272
273 fn deck_angles(&self, power: i32) -> (f32, f32) {
274 let p = self.p as i64;
275 let k = (power as i64).rem_euclid(p);
276 let step = TAU / self.p as f32;
277 (step * k as f32, step * ((k * self.q as i64) % p) as f32)
278 }
279
280 fn wedge_parameter(&self, x: Vec4) -> f32 {
281 x.y.atan2(x.x) * (self.p as f32 / TAU) + 0.5
282 }
283
284 fn wedge_offset(&self, x: Vec4) -> i32 {
285 -(self.wedge_parameter(x).floor() as i32)
286 }
287
288 fn nearest_lift(&self, from: Vec4, to: Vec4) -> (i32, Vec4, f32) {
289 let mut best_lift = self.cover_apply(self.deck(0), to);
290 let mut best_power = 0;
291 let mut best_distance = COVER.distance(from, best_lift);
292 for power in 1..self.p as i32 {
293 let lift = self.cover_apply(self.deck(power), to);
294 let distance = COVER.distance(from, lift);
295 if distance < best_distance {
296 best_lift = lift;
297 best_power = power;
298 best_distance = distance;
299 }
300 }
301 (best_power, best_lift, best_distance)
302 }
303
304 pub fn wgsl_prelude(&self) -> String {
306 let p = self.p;
307 let wedges_per_turn = self.p as f32 / TAU;
308 let table = (0..self.p as i32)
309 .map(|power| {
310 let m = self.deck(power).matrix;
311 format!(
312 "vec4<f32>({:?}, {:?}, {:?}, {:?})",
313 m.x_axis.x, m.x_axis.y, m.z_axis.z, m.z_axis.w
314 )
315 })
316 .collect::<Vec<_>>()
317 .join(",\n ");
318 format!(
319 r#"
320
321const LOAM_LENS_P: i32 = {p};
322const LOAM_LENS_WEDGES_PER_TURN: f32 = {wedges_per_turn:?};
323const LOAM_LENS_DECK = array<vec4<f32>, {p}>(
324 {table}
325);
326
327fn loam_lens_apply(x: vec4<f32>, power: i32) -> vec4<f32> {{
328 let k = ((power % LOAM_LENS_P) + LOAM_LENS_P) % LOAM_LENS_P;
329 let d = LOAM_LENS_DECK[k];
330 return normalize(vec4<f32>(
331 x.x * d.x - x.y * d.y,
332 x.x * d.y + x.y * d.x,
333 x.z * d.z - x.w * d.w,
334 x.z * d.w + x.w * d.z,
335 ));
336}}
337
338fn loam_lens_wedge_offset(x: vec4<f32>) -> i32 {{
339 return -i32(floor(atan2(x.y, x.x) * LOAM_LENS_WEDGES_PER_TURN + 0.5));
340}}
341
342fn loam_lens_wrap(x: vec4<f32>) -> vec4<f32> {{
343 var power = loam_lens_wedge_offset(x);
344 var wrapped = loam_lens_apply(x, power);
345 let correction = loam_lens_wedge_offset(wrapped);
346 if correction != 0 {{
347 power += correction;
348 wrapped = loam_lens_apply(x, power);
349 }}
350 return wrapped;
351}}
352
353fn loam_lens_distance(a: vec4<f32>, b: vec4<f32>) -> f32 {{
354 var best = 2.0;
355 for (var k = 0; k < LOAM_LENS_P; k = k + 1) {{
356 best = min(best, length(a - loam_lens_apply(b, k)) * 0.5);
357 }}
358 return 2.0 * asin(clamp(best, 0.0, 1.0));
359}}
360
361fn loam_lens_nearest_power(a: vec4<f32>, b: vec4<f32>) -> i32 {{
362 var best = 2.0;
363 var power = 0;
364 for (var k = 0; k < LOAM_LENS_P; k = k + 1) {{
365 let half_chord = length(a - loam_lens_apply(b, k)) * 0.5;
366 if half_chord < best {{
367 best = half_chord;
368 power = k;
369 }}
370 }}
371 return power;
372}}
373"#
374 )
375 }
376}
377
378fn gcd(a: u32, b: u32) -> u32 {
380 if b == 0 {
381 a
382 } else {
383 gcd(b, a % b)
384 }
385}
386
387impl Space for LensSpace {
388 type Point = Vec4;
389 type Vector = Vec4;
390 type Frame = Iso4;
391
392 fn frame_at(&self, at: Vec4) -> Iso4 {
393 Iso4::left_translation(at)
394 }
395
396 fn distance(&self, a: Vec4, b: Vec4) -> f32 {
397 self.nearest_lift(a, b).2
398 }
399
400 fn exp(&self, at: Vec4, v: Vec4) -> Vec4 {
401 self.wrap_to_domain(COVER.exp(at, v)).0
402 }
403
404 fn log(&self, from: Vec4, to: Vec4) -> Vec4 {
405 let (_, lift, _) = self.nearest_lift(from, to);
406 COVER.log(from, lift)
407 }
408
409 fn parallel_transport(&self, from: Vec4, to: Vec4, v: Vec4) -> Vec4 {
410 let (power, lift, _) = self.nearest_lift(from, to);
411 let carried = COVER.parallel_transport(from, lift, v);
412
413 self.cover_transport(self.deck(-power), lift, carried)
414 }
415
416 fn chart_envelope(&self) -> f32 {
417 COVER.chart_envelope()
418 }
419
420 fn valid_point(&self, p: Vec4) -> bool {
421 COVER.valid_point(p)
422 }
423}
424
425impl CoveringSpace for LensSpace {
426 type CoverIso = Iso4;
427
428 fn cover_identity(&self) -> Iso4 {
429 COVER.iso_identity()
430 }
431
432 fn cover_compose(&self, a: Iso4, b: Iso4) -> Iso4 {
433 COVER.iso_compose(a, b)
434 }
435
436 fn cover_inverse(&self, a: Iso4) -> Iso4 {
437 COVER.iso_inverse(a)
438 }
439
440 fn cover_apply(&self, iso: Iso4, p: Vec4) -> Vec4 {
441 COVER.iso_apply(iso, p)
442 }
443
444 fn cover_transport(&self, iso: Iso4, at: Vec4, v: Vec4) -> Vec4 {
445 COVER.iso_transport(iso, at, v)
446 }
447}
448
449impl QuotientSpace for LensSpace {
450 fn face_pairings(&self) -> impl Iterator<Item = Iso4> {
451 std::iter::once(self.deck(1))
452 }
453
454 fn in_fundamental_domain(&self, p: Vec4) -> bool {
455 (0.0..1.0).contains(&self.wedge_parameter(p))
456 }
457
458 fn wrap_to_domain(&self, p: Vec4) -> (Vec4, Iso4) {
459 let mut power = self.wedge_offset(p);
460 let mut deck = self.deck(power);
461 let mut wrapped = self.cover_apply(deck, p);
462
463 let correction = self.wedge_offset(wrapped);
464 if correction != 0 {
465 power += correction;
466 deck = self.deck(power);
467 wrapped = self.cover_apply(deck, p);
468 }
469 (wrapped, deck)
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use approx::assert_relative_eq;
477
478 const CELL: Vec3 = Vec3::new(2.0, 3.0, 1.5);
479
480 fn torus() -> FlatTorus3 {
481 FlatTorus3::new(CELL)
482 }
483
484 pub(super) struct Xorshift(pub(super) u32);
485
486 impl Xorshift {
487 pub(super) fn next_u32(&mut self) -> u32 {
488 self.0 ^= self.0 << 13;
489 self.0 ^= self.0 >> 17;
490 self.0 ^= self.0 << 5;
491 self.0
492 }
493
494 pub(super) fn signed(&mut self, span: f32) -> f32 {
495 let unit = self.next_u32() as f32 / u32::MAX as f32;
496 (unit * 2.0 - 1.0) * span
497 }
498
499 fn point(&mut self, span: f32) -> Vec3 {
500 Vec3::new(self.signed(span), self.signed(span), self.signed(span))
501 }
502 }
503
504 fn boundary_lifts(cell: Vec3) -> Vec<Vec3> {
505 let half = 0.5 * cell;
506 let mut lifts = Vec::new();
507 for axis in 0..3 {
508 for sign in [-1.0_f32, 1.0] {
509 let edge = sign * half[axis];
510 for coordinate in [
511 edge,
512 f32::from_bits(edge.to_bits() - 1),
513 f32::from_bits(edge.to_bits() + 1),
514 ] {
515 for copy in -3..=3 {
516 let mut p = Vec3::new(0.25, -0.5, 0.125);
517 p[axis] = coordinate + copy as f32 * cell[axis];
518 lifts.push(p);
519 }
520 }
521 }
522 }
523 lifts
524 }
525
526 #[test]
527 fn wrap_lands_every_lift_in_the_half_open_fundamental_domain() {
528 let t = torus();
529 let mut rng = Xorshift(0x5eed_1234);
530 let mut lifts = boundary_lifts(CELL);
531 for _ in 0..200_000 {
532 lifts.push(rng.point(40.0));
533 }
534 for p in lifts {
535 let (q, _) = t.wrap_to_domain(p);
536 assert!(
537 t.in_fundamental_domain(q),
538 "wrap({p:?}) = {q:?} escaped the domain of cell {CELL:?}"
539 );
540 }
541 }
542
543 #[test]
544 fn fundamental_domain_membership_is_exactly_the_wrap_fixpoint() {
545 let t = torus();
546 let mut rng = Xorshift(0x1337_beef);
547 for _ in 0..50_000 {
548 let p = rng.point(6.0);
549 let (q, _) = t.wrap_to_domain(p);
550 assert_eq!(t.in_fundamental_domain(p), p == q, "at {p:?}");
551 }
552 }
553
554 #[test]
555 fn the_returned_deck_element_reproduces_the_representative_exactly() {
556 let t = torus();
557 let mut rng = Xorshift(0x0bad_f00d);
558 let mut lifts = boundary_lifts(CELL);
559 for _ in 0..50_000 {
560 lifts.push(rng.point(25.0));
561 }
562 for p in lifts {
563 let (q, g) = t.wrap_to_domain(p);
564 assert_eq!(t.cover_apply(g, p), q, "deck element disagreed at {p:?}");
565 }
566 }
567
568 #[test]
569 fn wrapping_an_interior_point_returns_the_identity_deck_element() {
570 let t = torus();
571 let p = Vec3::new(0.3, -1.1, 0.4);
572 let (q, g) = t.wrap_to_domain(p);
573 assert_eq!(q, p);
574 assert_eq!(g, Iso3::IDENTITY);
575 }
576
577 #[test]
578 fn deck_group_composition_is_a_group_action_on_lifts() {
579 let t = torus();
580 let generators: Vec<Iso3> = t
581 .face_pairings()
582 .flat_map(|g| [g, t.cover_inverse(g)])
583 .collect();
584 let mut rng = Xorshift(0xfeed_face);
585
586 for _ in 0..20_000 {
587 let a = word(&t, &generators, &mut rng);
588 let b = word(&t, &generators, &mut rng);
589 let p = rng.point(8.0);
590
591 let sequential = t.cover_apply(a, t.cover_apply(b, p));
592 let composed = t.cover_apply(t.cover_compose(a, b), p);
593 assert_relative_eq!(sequential.x, composed.x, epsilon = 1e-5);
594 assert_relative_eq!(sequential.y, composed.y, epsilon = 1e-5);
595 assert_relative_eq!(sequential.z, composed.z, epsilon = 1e-5);
596
597 assert_eq!(t.cover_apply(t.cover_identity(), p), p);
598
599 let round_trip = t.cover_apply(t.cover_inverse(a), t.cover_apply(a, p));
600 assert_relative_eq!(round_trip.x, p.x, epsilon = 1e-5);
601 assert_relative_eq!(round_trip.y, p.y, epsilon = 1e-5);
602 assert_relative_eq!(round_trip.z, p.z, epsilon = 1e-5);
603 }
604 }
605
606 fn word(t: &FlatTorus3, generators: &[Iso3], rng: &mut Xorshift) -> Iso3 {
607 let length = (rng.next_u32() % 5) as usize;
608 (0..length).fold(t.cover_identity(), |accumulated, _| {
609 let pick = generators[(rng.next_u32() as usize) % generators.len()];
610 t.cover_compose(accumulated, pick)
611 })
612 }
613
614 #[test]
615 fn wrap_is_invariant_under_the_deck_group() {
616 let t = torus();
617 let generators: Vec<Iso3> = t
618 .face_pairings()
619 .flat_map(|g| [g, t.cover_inverse(g)])
620 .collect();
621 let mut rng = Xorshift(0x00c0_ffee);
622 for _ in 0..20_000 {
623 let p = rng.point(5.0);
624 let (q, _) = t.wrap_to_domain(p);
625 let g = word(&t, &generators, &mut rng);
626 let (q_translated, _) = t.wrap_to_domain(t.cover_apply(g, p));
627 assert_relative_eq!(q_translated.x, q.x, epsilon = 1e-4);
628 assert_relative_eq!(q_translated.y, q.y, epsilon = 1e-4);
629 assert_relative_eq!(q_translated.z, q.z, epsilon = 1e-4);
630 }
631 }
632
633 #[test]
634 fn distance_is_the_minimum_over_the_lattice() {
635 let t = torus();
636 let mut rng = Xorshift(0xdead_10cc);
637 for _ in 0..20_000 {
638 let a = rng.point(4.0);
639 let b = rng.point(4.0);
640 let center = ((a - b) / CELL).round();
641 let mut brute = f32::INFINITY;
642 for i in -3..=3 {
643 for j in -3..=3 {
644 for k in -3..=3 {
645 let offset = Vec3::new(i as f32, j as f32, k as f32);
646 brute = brute.min((a - b - CELL * (center + offset)).length());
647 }
648 }
649 }
650 assert_relative_eq!(t.distance(a, b), brute, epsilon = 1e-5);
651 }
652 }
653
654 #[test]
655 fn distance_is_lattice_invariant_symmetric_and_zero_on_the_orbit() {
656 let t = torus();
657 let generators: Vec<Iso3> = t
658 .face_pairings()
659 .flat_map(|g| [g, t.cover_inverse(g)])
660 .collect();
661 let mut rng = Xorshift(0x9e37_79b9);
662 for _ in 0..20_000 {
663 let a = rng.point(4.0);
664 let b = rng.point(4.0);
665 let g = word(&t, &generators, &mut rng);
666
667 assert_relative_eq!(t.distance(a, b), t.distance(b, a));
668 assert_relative_eq!(
669 t.distance(t.cover_apply(g, a), b),
670 t.distance(a, b),
671 epsilon = 1e-4
672 );
673 assert_relative_eq!(t.distance(a, t.cover_apply(g, a)), 0.0, epsilon = 1e-4);
674 }
675 }
676
677 #[test]
678 fn distance_satisfies_the_triangle_inequality() {
679 let t = torus();
680 let mut rng = Xorshift(0x7f4a_7c15);
681 for _ in 0..50_000 {
682 let a = rng.point(5.0);
683 let b = rng.point(5.0);
684 let c = rng.point(5.0);
685 assert!(t.distance(a, c) <= t.distance(a, b) + t.distance(b, c) + 1e-4);
686 }
687 }
688
689 #[test]
690 fn a_geodesic_crossing_the_gluing_lands_where_the_face_pairing_says() {
691 let t = torus();
692 let half = 0.5 * CELL;
693 let pairings: Vec<Iso3> = t.face_pairings().collect();
694
695 for (axis, pairing) in pairings.iter().enumerate() {
696 let mut start = Vec3::new(0.1, 0.9, -0.3);
697 start[axis] = half[axis] - 0.05;
698 let mut step = Vec3::ZERO;
699 step[axis] = 0.2;
700
701 let uncorrected = start + step;
702 let (arrived, deck) = t.wrap_to_domain(uncorrected);
703
704 let expected = t.cover_inverse(*pairing);
705 assert_eq!(deck.translation, expected.translation, "axis {axis}");
706 assert_eq!(t.cover_apply(expected, uncorrected), arrived);
707 assert_eq!(t.exp(start, step), arrived);
708 }
709 }
710
711 #[test]
712 fn stepped_ray_continuation_matches_a_single_geodesic_exp() {
713 let t = torus();
714 let mut rng = Xorshift(0x4d59_5f21);
715 for _ in 0..5_000 {
716 let start = t.wrap(rng.point(1.5));
717 let velocity = rng.point(4.0);
718
719 let direct = t.exp(start, velocity);
720
721 const STEPS: usize = 64;
722 let mut marched = start;
723 for _ in 0..STEPS {
724 marched = t.exp(marched, velocity / STEPS as f32);
725 }
726
727 let residual = t.distance(direct, marched);
728 assert!(
729 residual < 64.0 * f32::EPSILON * CELL.max_element() * 8.0,
730 "stepped {marched:?} vs direct {direct:?}, residual {residual}"
731 );
732 }
733 }
734
735 #[test]
736 fn exp_and_log_invert_exactly_within_the_injectivity_radius() {
737 let t = torus();
738 let radius = t.injectivity_radius();
739 assert_relative_eq!(radius, 0.75);
740
741 let mut rng = Xorshift(0x6c07_8965);
742 for _ in 0..20_000 {
743 let from = t.wrap(rng.point(2.0));
744 let direction = rng.point(1.0).normalize_or_zero();
745 let v = direction * (0.95 * radius);
746 let to = t.exp(from, v);
747 let recovered = t.log(from, to);
748 assert_relative_eq!(recovered.x, v.x, epsilon = 1e-5);
749 assert_relative_eq!(recovered.y, v.y, epsilon = 1e-5);
750 assert_relative_eq!(recovered.z, v.z, epsilon = 1e-5);
751 }
752
753 let short = t.log(Vec3::ZERO, t.exp(Vec3::ZERO, Vec3::new(0.0, 2.0, 0.0)));
754 assert_relative_eq!(short.x, 0.0);
755 assert_relative_eq!(short.y, -1.0);
756 assert_relative_eq!(short.z, 0.0);
757 }
758
759 #[test]
760 fn a_cover_rotation_does_not_claim_quotient_distance_invariance() {
761 let t = FlatTorus3::cube(2.0);
762 let a = Vec3::new(0.9, 0.0, 0.0);
763 let b = Vec3::new(-0.9, 0.0, 0.0);
764 let rotation = Iso3::from_rotation(Quat::from_rotation_z(PI / 4.0));
765 let moved = t.distance(t.cover_apply(rotation, a), t.cover_apply(rotation, b));
766 let deck = t.face_pairings().next().unwrap();
767
768 assert_relative_eq!(t.distance(a, b), 0.2, epsilon = 1e-6);
769 assert!(moved > 1.0);
770 assert_relative_eq!(t.distance(a, t.cover_apply(deck, a)), 0.0, epsilon = 1e-6);
771 assert_relative_eq!(
772 t.distance(t.cover_apply(deck, a), t.cover_apply(deck, b)),
773 t.distance(a, b),
774 epsilon = 1e-6
775 );
776 }
777
778 #[test]
779 #[should_panic(expected = "must be finite and positive")]
780 fn a_degenerate_cell_is_rejected_at_construction() {
781 FlatTorus3::new(Vec3::new(1.0, 0.0, 1.0));
782 }
783
784 #[test]
785 fn wgsl_prelude_exports_the_abi_and_bakes_a_round_tripping_cell() {
786 let cell = Vec3::new(2.0, 3.0, 1.5e-7);
787 let source = FlatTorus3::new(cell).wgsl_impl().into_owned();
788 for symbol in [
789 "fn loam_distance(",
790 "fn loam_origin_distance(",
791 "fn loam_exp(",
792 "fn loam_log(",
793 "fn loam_geodesic_step(",
794 "fn loam_torus_wrap(",
795 "const LOAM_MAX_ARC",
796 ] {
797 assert!(source.contains(symbol), "missing {symbol}");
798 }
799
800 let baked = source
801 .split("const LOAM_TORUS_CELL: vec3<f32> = vec3<f32>(")
802 .nth(1)
803 .and_then(|rest| rest.split(')').next())
804 .expect("cell is baked");
805 let parsed: Vec<f32> = baked
806 .split(", ")
807 .map(|c| c.parse().expect("cell literal parses as f32"))
808 .collect();
809 assert_eq!(parsed, vec![cell.x, cell.y, cell.z], "emitted {baked}");
810 }
811}
812
813#[cfg(test)]
814mod lens_tests {
815 use super::tests::Xorshift;
816 use super::*;
817 use approx::assert_relative_eq;
818
819 const LENSES: &[(u32, u32)] = &[(2, 1), (3, 1), (5, 2), (7, 3), (8, 3)];
820
821 fn unit4(rng: &mut Xorshift) -> Vec4 {
822 let raw = Vec4::new(
823 rng.signed(1.0),
824 rng.signed(1.0),
825 rng.signed(1.0),
826 rng.signed(1.0),
827 );
828 if raw.length_squared() < 1e-6 {
829 Vec4::X
830 } else {
831 raw.normalize()
832 }
833 }
834
835 fn generator_by_hand(p: u32, q: u32) -> Mat4 {
836 let step = TAU / p as f32;
837 let (sin1, cos1) = step.sin_cos();
838 let (sin2, cos2) = (step * q as f32).sin_cos();
839 Mat4::from_cols(
840 Vec4::new(cos1, sin1, 0.0, 0.0),
841 Vec4::new(-sin1, cos1, 0.0, 0.0),
842 Vec4::new(0.0, 0.0, cos2, sin2),
843 Vec4::new(0.0, 0.0, -sin2, cos2),
844 )
845 }
846
847 const WALL_MARGIN: f32 = 1e-4;
848
849 fn wall_lifts(lens: &LensSpace) -> Vec<Vec4> {
850 let mut lifts = Vec::new();
851 for wedge in 0..lens.p() as i32 {
852 let wall = (wedge as f32 + 0.5) * TAU / lens.p() as f32;
853 for angle in [wall - WALL_MARGIN, wall + WALL_MARGIN] {
854 let (sin, cos) = angle.sin_cos();
855 lifts.push(Vec4::new(cos * 0.8, sin * 0.8, 0.5, 0.331_662_5).normalize());
856 }
857 }
858 lifts
859 }
860
861 #[test]
862 fn the_face_pairing_composes_to_the_identity_after_exactly_p_steps() {
863 let mut rng = Xorshift(0x1e45_9ac1);
864 for &(p, q) in LENSES {
865 let lens = LensSpace::new(p, q);
866 let generator = lens.face_pairings().next().expect("one face pairing");
867 let samples: Vec<Vec4> = (0..32).map(|_| unit4(&mut rng)).collect();
868
869 let mut accumulated = lens.cover_identity();
870 for step in 1..=p {
871 accumulated = lens.cover_compose(accumulated, generator);
872 let moved = samples
873 .iter()
874 .map(|&x| SphericalS3Embedded.distance(lens.cover_apply(accumulated, x), x))
875 .fold(0.0f32, f32::max);
876 if step == p {
877 assert!(
878 moved < 1e-5,
879 "L({p}, {q}): g^{step} moved a lift by {moved}, so the loop \
880 does not close"
881 );
882 } else {
883 assert!(
884 moved > 0.1,
885 "L({p}, {q}): g^{step} already acts trivially, so the deck \
886 group has order {step}, not {p}"
887 );
888 }
889 }
890 }
891 }
892
893 #[test]
894 fn deck_powers_agree_with_repeated_composition_of_the_generator() {
895 let mut rng = Xorshift(0x510c_2b77);
896 for &(p, q) in LENSES {
897 let lens = LensSpace::new(p, q);
898 let by_hand = generator_by_hand(p, q);
899 for power in -3 * p as i32..=3 * p as i32 {
900 let mut composed = Mat4::IDENTITY;
901 for _ in 0..power.rem_euclid(p as i32) {
902 composed = by_hand * composed;
903 }
904 for _ in 0..4 {
905 let x = unit4(&mut rng);
906 let want = (composed * x).normalize();
907 let got = lens.cover_apply(lens.deck(power), x);
908 assert!(
909 SphericalS3Embedded.distance(got, want) < 1e-4,
910 "L({p}, {q}): deck({power}) disagrees with the composed \
911 generator at {x:?}"
912 );
913 }
914 }
915 }
916 }
917
918 #[test]
919 #[should_panic(expected = "gcd(p, q) = 1")]
920 fn an_action_with_fixed_points_is_rejected_at_construction() {
921 LensSpace::new(6, 2);
922 }
923
924 #[test]
925 #[should_panic(expected = "p >= 2")]
926 fn the_trivial_quotient_is_rejected_at_construction() {
927 LensSpace::new(1, 0);
928 }
929
930 #[test]
931 fn wrap_lands_every_lift_in_the_half_open_fundamental_domain() {
932 let mut rng = Xorshift(0x5eed_1234);
933 for &(p, q) in LENSES {
934 let lens = LensSpace::new(p, q);
935 let mut lifts = wall_lifts(&lens);
936 lifts.extend((0..50_000).map(|_| unit4(&mut rng)));
937 for x in lifts {
938 let (wrapped, _) = lens.wrap_to_domain(x);
939 assert!(
940 lens.in_fundamental_domain(wrapped),
941 "L({p}, {q}): wrap({x:?}) = {wrapped:?} escaped the domain"
942 );
943 }
944 }
945 }
946
947 #[test]
948 fn fundamental_domain_membership_is_exactly_the_wrap_fixpoint() {
949 let mut rng = Xorshift(0x1337_beef);
950 for &(p, q) in LENSES {
951 let lens = LensSpace::new(p, q);
952 let mut lifts = wall_lifts(&lens);
953 lifts.extend((0..20_000).map(|_| unit4(&mut rng)));
954 for x in lifts {
955 let (wrapped, deck) = lens.wrap_to_domain(x);
956 assert_eq!(
957 lens.in_fundamental_domain(x),
958 deck.matrix == Mat4::IDENTITY,
959 "L({p}, {q}) at {x:?}"
960 );
961 if lens.in_fundamental_domain(x) {
962 assert!(SphericalS3Embedded.distance(wrapped, x) < 1e-6);
963 }
964 }
965 }
966 }
967
968 #[test]
969 fn a_lift_on_a_wedge_wall_comes_back_on_a_wall() {
970 for &(p, q) in LENSES {
971 let lens = LensSpace::new(p, q);
972 let half = PI / p as f32;
973 for wedge in 0..p as i32 {
974 let wall = (wedge as f32 + 0.5) * TAU / p as f32;
975 for angle in [
976 wall,
977 f32::from_bits(wall.to_bits() - 1),
978 f32::from_bits(wall.to_bits() + 1),
979 ] {
980 let (sin, cos) = angle.sin_cos();
981 let x = Vec4::new(cos * 0.8, sin * 0.8, 0.5, 0.331_662_5).normalize();
982 let (wrapped, deck) = lens.wrap_to_domain(x);
983 assert_eq!(lens.cover_apply(deck, x), wrapped, "L({p}, {q}) at {angle}");
984 assert!(
985 (wrapped.y.atan2(wrapped.x).abs() - half).abs() < 1e-5,
986 "L({p}, {q}): a wall lift came back at {} rad, not on a wall \
987 at {half}",
988 wrapped.y.atan2(wrapped.x)
989 );
990 }
991 }
992 }
993 }
994
995 #[test]
996 fn the_returned_deck_element_reproduces_the_representative_exactly() {
997 let mut rng = Xorshift(0x0bad_f00d);
998 for &(p, q) in LENSES {
999 let lens = LensSpace::new(p, q);
1000 let mut lifts = wall_lifts(&lens);
1001 lifts.extend((0..20_000).map(|_| unit4(&mut rng)));
1002 for x in lifts {
1003 let (wrapped, deck) = lens.wrap_to_domain(x);
1004 assert_eq!(
1005 lens.cover_apply(deck, x),
1006 wrapped,
1007 "L({p}, {q}): deck element disagreed at {x:?}"
1008 );
1009 }
1010 }
1011 }
1012
1013 #[test]
1014 fn wrap_is_invariant_under_the_deck_group() {
1015 let mut rng = Xorshift(0x00c0_ffee);
1016 for &(p, q) in LENSES {
1017 let lens = LensSpace::new(p, q);
1018 for _ in 0..20_000 {
1019 let x = unit4(&mut rng);
1020 let power = (rng.next_u32() % (4 * p)) as i32 - 2 * p as i32;
1021 let (wrapped, _) = lens.wrap_to_domain(x);
1022 let (translated, _) = lens.wrap_to_domain(lens.cover_apply(lens.deck(power), x));
1023 assert!(
1024 SphericalS3Embedded.distance(wrapped, translated) < 1e-3,
1025 "L({p}, {q}): wrap of g^{power} x is a different lift at {x:?}"
1026 );
1027 }
1028 }
1029 }
1030
1031 #[test]
1032 fn crossing_a_face_re_enters_through_the_opposite_face() {
1033 let mut rng = Xorshift(0x4d59_5f21);
1034 for &(p, q) in LENSES {
1035 let lens = LensSpace::new(p, q);
1036 let pairing = lens.face_pairings().next().expect("one face pairing");
1037 let half = PI / p as f32;
1038 let overshoot = 0.01;
1039
1040 let (sin, cos) = (half + overshoot).sin_cos();
1041 let outside = Vec4::new(cos * 0.8, sin * 0.8, 0.5, 0.331_662_5).normalize();
1042 let (wrapped, deck) = lens.wrap_to_domain(outside);
1043
1044 assert!(lens.in_fundamental_domain(wrapped));
1045 assert_relative_eq!(
1046 wrapped.y.atan2(wrapped.x),
1047 -half + overshoot,
1048 epsilon = 1e-5
1049 );
1050
1051 let inverse = lens.cover_inverse(pairing);
1052 for _ in 0..64 {
1053 let x = unit4(&mut rng);
1054 assert!(
1055 SphericalS3Embedded
1056 .distance(lens.cover_apply(inverse, x), lens.cover_apply(deck, x))
1057 < 1e-5,
1058 "L({p}, {q}): the crossing's deck element is not the face pairing"
1059 );
1060 }
1061 }
1062 }
1063
1064 #[test]
1065 fn distance_is_the_minimum_over_the_deck_group() {
1066 let mut rng = Xorshift(0xdead_10cc);
1067 for &(p, q) in LENSES {
1068 let lens = LensSpace::new(p, q);
1069 let generator = generator_by_hand(p, q);
1070 for _ in 0..5_000 {
1071 let a = unit4(&mut rng);
1072 let b = unit4(&mut rng);
1073 let mut brute = f32::INFINITY;
1074 let mut lift = b;
1075 for _ in 0..p {
1076 brute = brute.min(SphericalS3Embedded.distance(a, lift.normalize()));
1077 lift = generator * lift;
1078 }
1079 assert_relative_eq!(lens.distance(a, b), brute, epsilon = 1e-4);
1080 }
1081 }
1082 }
1083
1084 #[test]
1085 fn distance_is_deck_invariant_symmetric_and_zero_on_the_orbit() {
1086 let mut rng = Xorshift(0x9e37_79b9);
1087 for &(p, q) in LENSES {
1088 let lens = LensSpace::new(p, q);
1089 for _ in 0..5_000 {
1090 let a = unit4(&mut rng);
1091 let b = unit4(&mut rng);
1092 let power = (rng.next_u32() % (2 * p)) as i32;
1093 let moved = lens.cover_apply(lens.deck(power), a);
1094
1095 assert_relative_eq!(lens.distance(a, b), lens.distance(b, a), epsilon = 1e-5);
1096 assert_relative_eq!(lens.distance(moved, b), lens.distance(a, b), epsilon = 1e-4);
1097 assert!(lens.distance(a, moved) < 1e-3);
1098 }
1099 }
1100 }
1101
1102 #[test]
1103 fn distance_satisfies_the_triangle_inequality() {
1104 let mut rng = Xorshift(0x7f4a_7c15);
1105 for &(p, q) in LENSES {
1106 let lens = LensSpace::new(p, q);
1107 for _ in 0..5_000 {
1108 let a = unit4(&mut rng);
1109 let b = unit4(&mut rng);
1110 let c = unit4(&mut rng);
1111 assert!(lens.distance(a, c) <= lens.distance(a, b) + lens.distance(b, c) + 1e-4);
1112 }
1113 }
1114 }
1115
1116 #[test]
1117 fn the_injectivity_radius_is_half_the_shortest_deck_displacement() {
1118 let mut rng = Xorshift(0x41c6_4e6d);
1119 for &(p, q) in LENSES {
1120 let lens = LensSpace::new(p, q);
1121 let mut shortest = f32::INFINITY;
1122 for _ in 0..20_000 {
1123 let x = unit4(&mut rng);
1124 for power in 1..p as i32 {
1125 let moved = lens.cover_apply(lens.deck(power), x);
1126 shortest = shortest.min(SphericalS3Embedded.distance(x, moved));
1127 }
1128 }
1129 assert_relative_eq!(shortest, TAU / p as f32, epsilon = 2e-3);
1130 assert_relative_eq!(lens.injectivity_radius(), 0.5 * shortest, epsilon = 1e-3);
1131 }
1132 }
1133
1134 #[test]
1135 fn exp_and_log_invert_within_the_injectivity_radius() {
1136 let mut rng = Xorshift(0x6c07_8965);
1137 for &(p, q) in LENSES {
1138 let lens = LensSpace::new(p, q);
1139 let radius = lens.injectivity_radius();
1140 for _ in 0..5_000 {
1141 let at = unit4(&mut rng);
1142 let raw = unit4(&mut rng);
1143 let tangent = raw - raw.dot(at) * at;
1144 if tangent.length_squared() < 1e-6 {
1145 continue;
1146 }
1147 let v = tangent.normalize() * (0.9 * radius);
1148 let recovered = lens.log(at, lens.exp(at, v));
1149 assert!(
1150 (recovered - v).length() < 1e-4,
1151 "L({p}, {q}): log(exp(v)) missed v by {}",
1152 (recovered - v).length()
1153 );
1154 }
1155
1156 let at = Vec4::X;
1157 let v = Vec4::Y * (TAU / p as f32);
1158 assert!(lens.distance(at, lens.exp(at, v)) < 1e-3);
1159 assert!(lens.log(at, lens.exp(at, v)).length() < 1e-3);
1160 }
1161 }
1162
1163 #[test]
1164 fn parallel_transport_lands_tangent_to_the_lift_the_caller_named() {
1165 let mut rng = Xorshift(0x339a_cb15);
1166 for &(p, q) in LENSES {
1167 let lens = LensSpace::new(p, q);
1168 for _ in 0..5_000 {
1169 let from = unit4(&mut rng);
1170 let to = unit4(&mut rng);
1171 let raw = unit4(&mut rng);
1172 let v = raw - raw.dot(from) * from;
1173 let carried = lens.parallel_transport(from, to, v);
1174 assert!(
1175 carried.dot(to).abs() < 1e-4,
1176 "L({p}, {q}): transported vector is not tangent at `to`"
1177 );
1178 assert_relative_eq!(carried.length(), v.length(), epsilon = 1e-4);
1179 }
1180 }
1181 }
1182
1183 #[test]
1184 fn the_wgsl_prelude_bakes_the_deck_table_the_rust_impl_computes() {
1185 for &(p, q) in LENSES {
1186 let lens = LensSpace::new(p, q);
1187 let source = lens.wgsl_prelude();
1188 for symbol in [
1189 "fn loam_lens_apply(",
1190 "fn loam_lens_wedge_offset(",
1191 "fn loam_lens_wrap(",
1192 "fn loam_lens_distance(",
1193 "fn loam_lens_nearest_power(",
1194 "const LOAM_LENS_P",
1195 "const LOAM_LENS_WEDGES_PER_TURN",
1196 ] {
1197 assert!(source.contains(symbol), "L({p}, {q}) is missing {symbol}");
1198 }
1199
1200 let table = source
1201 .split("const LOAM_LENS_DECK = array<vec4<f32>, ")
1202 .nth(1)
1203 .and_then(|rest| rest.split(");").next())
1204 .expect("the deck table is baked");
1205 assert_eq!(
1206 table.matches("vec4<f32>(").count(),
1207 p as usize,
1208 "L({p}, {q}) baked {table}"
1209 );
1210
1211 for (power, entry) in table.split("vec4<f32>(").skip(1).enumerate() {
1212 let baked: Vec<f32> = entry
1213 .split(')')
1214 .next()
1215 .expect("a closed entry")
1216 .split(", ")
1217 .map(|coefficient| coefficient.parse().expect("an f32 literal"))
1218 .collect();
1219 let m = lens.deck(power as i32).matrix;
1220 assert_eq!(
1221 baked,
1222 vec![m.x_axis.x, m.x_axis.y, m.z_axis.z, m.z_axis.w],
1223 "L({p}, {q}): baked power {power} is not the Rust matrix"
1224 );
1225 }
1226 }
1227 }
1228}