Skip to main content

diffable/
flat.rs

1//! Flat manifolds obtained from Euclidean spaces by discrete identifications.
2//!
3//! [`S1`] is the quotient `R/Z`; [`Torus`] combines two circles with straight
4//! gluing, while [`KleinBottle`] twists one identification. Their cover types
5//! implement the [`Bounded`] machinery used by [`NerveComplex`] to recover
6//! global topology.
7//!
8//! [`NerveComplex`]: crate::traits::simplicial::NerveComplex
9//! [`Bounded`]: crate::traits::simplicial::Bounded
10
11use crate::{
12    discrete::Z,
13    impl_lie_group_via_quotient,
14    traits::{Interval, Tensor},
15};
16use core::marker::PhantomData;
17
18use crate::traits::{Chart, Euclidean, Group, LieGroup, Quotient, Real, Smooth};
19use num_traits::{Euclid, NumCast, One, Zero, real::Real as _};
20
21/// The circle `S¹`, as the quotient of the line `V` by the integer lattice
22/// [`Z`]. One-dimensional (`From<[F; 1]>`).
23#[derive(Copy, Clone, Debug, PartialEq)]
24pub struct S1<V: Euclidean + From<[<V as Tensor>::F; 1]>>(V);
25
26impl<V: Euclidean<F: Real> + From<[V::F; 1]>> Interval for S1<V> {
27    type R = V::F;
28
29    fn interval_squared(&self, other: &Self) -> V::F {
30        self.to_local(other).unwrap().norm_squared()
31    }
32}
33
34impl<V: Euclidean<F: Real> + From<[<V as Tensor>::F; 1]>> Quotient<V, Z<V>, V> for S1<V> {
35    fn new(g: V) -> Self {
36        let one = V::F::one();
37        let mut d = g[0].rem_euclid(&one);
38        if d == one {
39            // floating point garbage check gotta do it :(
40            d = V::F::zero();
41        }
42        Self([d].into())
43    }
44
45    fn lift(&self) -> V {
46        // nearest representative to the identity (0), not just the
47        // canonical [0,1) one — reduce into (-1/2, 1/2] instead.
48        let half = V::F::one() / (V::F::one() + V::F::one());
49        let mut d = self.0[0];
50        if d > half {
51            d = d - V::F::one();
52        }
53        [d].into()
54    }
55
56    fn embed(h: Z<V>) -> V {
57        [<V::F as NumCast>::from(h.0).unwrap()].into()
58    }
59}
60
61impl_lie_group_via_quotient!(S1<V>, V, Z<V>, V, V: Euclidean + From<[<V as Tensor>::F; 1]>);
62
63/// The 2-torus `T² = S¹ × S¹`.
64#[derive(Debug, Copy, Clone, PartialEq)]
65pub struct Torus<I: Euclidean + From<[I::F; 1]> + From<[V::F; 1]>, V: Euclidean + From<[I::F; 2]>>(
66    S1<I>,
67    S1<I>,
68    PhantomData<V>,
69);
70
71impl<I: ICompatible<V>, V: VCompatible<I>> Torus<I, V> {
72    /// Constructs a torus point from its two circle coordinates.
73    pub fn new(a: S1<I>, b: S1<I>) -> Self {
74        Self(a, b, PhantomData)
75    }
76}
77
78impl<I: ICompatible<V>, V: VCompatible<I>> Interval for Torus<I, V>
79where
80    I::F: From<V::F>,
81{
82    type R = I::F;
83
84    fn interval_squared(&self, other: &Self) -> I::F {
85        self.to_local(other).unwrap().norm_squared().into()
86    }
87}
88
89impl<I: ICompatible<V>, V: VCompatible<I>> Group for Torus<I, V> {
90    fn identity() -> Self {
91        Self::new(S1::identity(), S1::identity())
92    }
93
94    fn compose(&self, other: &Self) -> Self {
95        Self::new(self.0.compose(&other.0), self.1.compose(&other.1))
96    }
97
98    fn inverse(&self) -> Self {
99        Self::new(self.0.inverse(), self.1.inverse())
100    }
101}
102
103impl<I: ICompatible<V>, V: VCompatible<I>> LieGroup<V> for Torus<I, V> {
104    fn identity_exp(v: V) -> Self {
105        let v0 = [v[0]].into();
106        let v1 = [v[1]].into();
107        Self::new(S1::identity_exp(v0), S1::identity_exp(v1))
108    }
109
110    fn identity_log(p: &Self) -> Option<V> {
111        let a = S1::identity_log(&p.0)?;
112        let b = S1::identity_log(&p.1)?;
113
114        Some([a[0], b[0]].into())
115    }
116}
117
118/// Dimensional-compatibility bound for the *inner circle* type of a
119/// [`KleinBottle`]/[`Torus`], relating it to the ambient type `V`. See
120/// [`VCompatible`] for the dual constraint.
121pub trait ICompatible<V: Euclidean<F: Real + Send + Sync> + From<[Self::F; 2]>>:
122    Euclidean<F: Real + From<V::F>> + From<[Self::F; 1]> + From<[V::F; 1]> + 'static + Send + Sync
123{
124}
125
126/// Dimensional-compatibility bound for the *ambient* type of a
127/// [`KleinBottle`]/[`Torus`], relating it to the inner circle type `I`.
128pub trait VCompatible<I: Euclidean<F: Real + From<Self::F>> + From<[I::F; 1]> + From<[Self::F; 1]>>:
129    Euclidean<F: Real + Send + Sync> + From<[I::F; 2]> + 'static + Send + Sync
130{
131}
132
133impl<
134    I: Euclidean<F: Real + From<V::F>> + From<[I::F; 1]> + From<[V::F; 1]> + 'static + Send + Sync,
135    V: Euclidean<F: Real + Send + Sync> + From<[I::F; 2]> + 'static + Send + Sync,
136> ICompatible<V> for I
137{
138}
139
140impl<
141    I: Euclidean<F: Real + From<V::F>> + From<[I::F; 1]> + From<[V::F; 1]> + 'static + Send + Sync,
142    V: Euclidean<F: Real + Send + Sync> + From<[I::F; 2]> + 'static + Send + Sync,
143> VCompatible<I> for V
144{
145}
146
147/// The Klein bottle — the non-orientable quotient of the plane, built as two
148/// circles with a twist. `I` is the "inner" circle coordinate type and `V` the
149/// ambient embedding type; [`ICompatible`]/[`VCompatible`] pin the dimensional
150/// relationship between them that the twist requires.
151#[derive(Debug, Copy, Clone, PartialEq)]
152pub struct KleinBottle<I: ICompatible<V>, V: VCompatible<I>>(S1<I>, S1<I>, PhantomData<V>);
153
154impl<I: ICompatible<V>, V: VCompatible<I>> KleinBottle<I, V> {
155    /// Constructs a Klein-bottle point from its two circle coordinates.
156    pub fn new(a: S1<I>, b: S1<I>) -> Self {
157        Self(a, b, PhantomData)
158    }
159}
160
161impl<I: ICompatible<V>, V: VCompatible<I>> Smooth<V> for KleinBottle<I, V> {
162    type Global = Self;
163
164    fn exp(&self, v: V) -> Self {
165        let (x, y) = self.coords();
166        let vx: I::F = v[0].into();
167        let vy: I::F = v[1].into();
168        Self::from_cover(x + vx, y + vy)
169    }
170
171    fn log(&self, other: &Self) -> Option<V> {
172        let one = I::F::one();
173        let two = one + one;
174        let (sx, sy) = self.coords();
175        let (ox, oy) = other.coords();
176        let mut best: Option<(I::F, I::F)> = None;
177        let mut best_sq = I::F::zero();
178
179        for n in [-one, I::F::zero(), one] {
180            let n_odd = n.rem_euclid(&two) != I::F::zero();
181            // Reflection formula in the (-1/2,1/2]-centered
182            // convention is `-ox`, not `1 - ox` — reflecting about
183            // 0 (the domain's center), not about 1/2 (which was
184            // only the reflection point under the old [0,1)
185            // convention).
186            let base_ox = if n_odd { -ox } else { ox };
187            for m in [-one, I::F::zero(), one] {
188                let cx = base_ox + m;
189                let cy = oy + n;
190                let dx = cx - sx;
191                let dy = cy - sy;
192                let sq = dx * dx + dy * dy;
193                if best.is_none() || sq < best_sq {
194                    best = Some((dx, dy));
195                    best_sq = sq;
196                }
197            }
198        }
199        best.map(|(dx, dy)| [dx, dy].into())
200    }
201}
202
203impl<I: ICompatible<V>, V: VCompatible<I>> KleinBottle<I, V> {
204    /// Reduce a cover point (x, y) ∈ ℝ² to the fundamental domain via
205    /// Γ = ⟨A, B⟩, A: (x,y) ↦ (x+1, y), B: (x,y) ↦ (−x, y+1).
206    ///
207    /// Uses the SAME (-1/2, 1/2]-centered convention as `S1::lift`
208    /// throughout: seam-crossing count is `y.round()` (nearest
209    /// integer), not `y.floor()`, since the fundamental domain is
210    /// centered at 0 rather than starting at 0. Parity of that
211    /// rounded count decides the flip, exactly as before — only the
212    /// rounding function and the reflection formula's center point
213    /// (0, not 1/2) changed.
214    fn from_cover(x: I::F, y: I::F) -> Self {
215        let one = I::F::one();
216        let two = one + one;
217        let ky = y.round(); // nearest-centered seam count
218        let y_red = y - ky; // in (-1/2, 1/2]
219
220        let ky_parity_odd = ky.rem_euclid(&two) != I::F::zero();
221        let x_oriented = if ky_parity_odd { -x } else { x };
222
223        // S1::new performs the (-1/2,1/2] reduction itself now, so
224        // x_oriented can be handed to it directly, unreduced.
225        Self(
226            S1::new([x_oriented].into()),
227            S1::new([y_red].into()),
228            PhantomData,
229        )
230    }
231
232    fn coords(&self) -> (I::F, I::F) {
233        (self.0.lift()[0], self.1.lift()[0])
234    }
235}
236
237impl<I: ICompatible<V>, V: VCompatible<I>> Interval for KleinBottle<I, V> {
238    type R = I::F;
239
240    fn interval_squared(&self, other: &Self) -> I::F {
241        self.to_local(other).unwrap().norm_squared().into()
242    }
243}
244
245#[cfg(feature = "simplicial")]
246mod simplicial {
247    use super::*;
248    use crate::{
249        impl_tangent_bundle_via_bounded,
250        traits::simplicial::{Bounded, BuildNodes, NerveComplexParameters},
251    };
252    use std::vec::Vec;
253    /// A bounded chart domain in the regular finite cover of [`Torus`].
254    #[derive(Debug, Copy, Clone)]
255    pub struct TorusCover<I: ICompatible<V>, V: VCompatible<I>>(Torus<I, V>);
256
257    impl<I: ICompatible<V>, V: VCompatible<I>> From<Torus<I, V>> for TorusCover<I, V> {
258        fn from(value: Torus<I, V>) -> Self {
259            Self(value)
260        }
261    }
262
263    impl<I: ICompatible<V>, V: VCompatible<I>> AsRef<Torus<I, V>> for TorusCover<I, V> {
264        fn as_ref(&self) -> &Torus<I, V> {
265            &self.0
266        }
267    }
268
269    const S: usize = 4;
270
271    impl<I: ICompatible<V>, V: VCompatible<I>> Bounded<Torus<I, V>, Torus<I, V>, V>
272        for TorusCover<I, V>
273    {
274        fn sdf(&self, v: &V) -> <V as Tensor>::F {
275            let to = |x| <V::F as NumCast>::from(x).unwrap();
276            v.norm() - (to(2).sqrt() + to(2)) / to(4 * S)
277        }
278    }
279
280    impl_tangent_bundle_via_bounded!(
281        TorusCover<I, V>,
282        Torus<I, V>,
283        Torus<I, V>,
284        V,
285        I: ICompatible<V>,
286    V: VCompatible<I>
287    );
288
289    impl<I: ICompatible<V>, V: VCompatible<I>> BuildNodes<TorusCover<I, V>> for TorusCover<I, V> {
290        fn build_nodes() -> Vec<Self> {
291            let to = |x| <I::F as NumCast>::from(x).unwrap();
292            let s = to(S);
293            let offset = to(1) / (to(2) * s);
294
295            (0..S)
296                .flat_map(|y| (0..S).map(move |x| (x, y)))
297                .map(|(x, y)| {
298                    Torus::new(
299                        S1([offset + to(x) / s].into()),
300                        S1([offset + to(y) / s].into()),
301                    )
302                    .into()
303                })
304                .collect()
305        }
306    }
307
308    impl<I: ICompatible<V>, V: VCompatible<I>>
309        NerveComplexParameters<Torus<I, V>, V, Torus<I, V>, TorusCover<I, V>> for TorusCover<I, V>
310    {
311    }
312
313    /// A bounded chart domain in the regular finite cover of [`KleinBottle`].
314    #[derive(Debug, Copy, Clone)]
315    pub struct KleinBottleCover<I: ICompatible<V>, V: VCompatible<I>>(KleinBottle<I, V>);
316
317    impl<I: ICompatible<V>, V: VCompatible<I>> From<KleinBottle<I, V>> for KleinBottleCover<I, V> {
318        fn from(value: KleinBottle<I, V>) -> Self {
319            Self(value)
320        }
321    }
322
323    impl<I: ICompatible<V>, V: VCompatible<I>> AsRef<KleinBottle<I, V>> for KleinBottleCover<I, V> {
324        fn as_ref(&self) -> &KleinBottle<I, V> {
325            &self.0
326        }
327    }
328
329    impl<I: ICompatible<V>, V: VCompatible<I>> Bounded<KleinBottle<I, V>, KleinBottle<I, V>, V>
330        for KleinBottleCover<I, V>
331    {
332        fn sdf(&self, v: &V) -> <V as Tensor>::F {
333            let to = |x| <V::F as NumCast>::from(x).unwrap();
334            v.norm() - (to(2).sqrt() + to(2)) / to(4 * S)
335        }
336    }
337
338    impl_tangent_bundle_via_bounded!(
339        KleinBottleCover<I, V>,
340        KleinBottle<I, V>,
341        KleinBottle<I, V>,
342        V,
343        I: ICompatible<V>, V: VCompatible<I>
344    );
345
346    impl<I: ICompatible<V>, V: VCompatible<I>> BuildNodes<KleinBottleCover<I, V>>
347        for KleinBottleCover<I, V>
348    {
349        fn build_nodes() -> Vec<Self> {
350            let to = |x| <I::F as NumCast>::from(x).unwrap();
351            let s = to(S);
352            let offset = to(1) / (to(2) * s);
353
354            (0..S)
355                .flat_map(|y| (0..S).map(move |x| (x, y)))
356                .map(|(x, y)| {
357                    KleinBottle::new(
358                        S1([offset + to(x) / s].into()),
359                        S1([offset + to(y) / s].into()),
360                    )
361                    .into()
362                })
363                .collect()
364        }
365    }
366
367    impl<I: ICompatible<V>, V: VCompatible<I>>
368        NerveComplexParameters<KleinBottle<I, V>, V, KleinBottle<I, V>, KleinBottleCover<I, V>>
369        for KleinBottleCover<I, V>
370    {
371    }
372
373    /// A deliberately overlapping bounded domain on [`Torus`].
374    ///
375    /// This exercises [`NerveComplex`](crate::traits::simplicial::NerveComplex)
376    /// when many cover nodes see the same region rather than forming the regular
377    /// cover represented by [`TorusCover`].
378    #[derive(Debug, Clone)]
379    pub struct MyopicTorus<I: ICompatible<V>, V: VCompatible<I>>(pub Torus<I, V>);
380
381    impl<I: ICompatible<V>, V: VCompatible<I>> MyopicTorus<I, V> {
382        /// Returns the number of cover samples along each torus coordinate.
383        pub fn s() -> usize {
384            8
385        }
386
387        fn radius() -> V::F {
388            // 2/s, quite a lot larger than the lattice spacing.
389            (V::F::one() + V::F::one()) / <V::F as NumCast>::from(Self::s()).unwrap()
390        }
391    }
392
393    impl<I: ICompatible<V>, V: VCompatible<I>> AsRef<Torus<I, V>> for MyopicTorus<I, V> {
394        fn as_ref(&self) -> &Torus<I, V> {
395            &self.0
396        }
397    }
398
399    impl<I: ICompatible<V>, V: VCompatible<I>> From<Torus<I, V>> for MyopicTorus<I, V> {
400        fn from(value: Torus<I, V>) -> Self {
401            Self(value)
402        }
403    }
404
405    impl<I: ICompatible<V>, V: VCompatible<I>> Bounded<Torus<I, V>, Torus<I, V>, V>
406        for MyopicTorus<I, V>
407    {
408        fn sdf(&self, v: &V) -> <V as Tensor>::F {
409            v.norm() - Self::radius()
410        }
411    }
412
413    impl_tangent_bundle_via_bounded!(
414        MyopicTorus<I, V>,
415        Torus<I, V>,
416        Torus<I, V>,
417        V,
418        I: ICompatible<V>, V: VCompatible<I>
419    );
420
421    /// The tangent-bundle chart wrapper associated with [`MyopicTorus`].
422    #[derive(Debug, Clone)]
423    pub struct MyopicTorusCover<I: ICompatible<V>, V: VCompatible<I>>(MyopicTorus<I, V>);
424
425    impl<I: ICompatible<V>, V: VCompatible<I>> AsRef<MyopicTorus<I, V>> for MyopicTorusCover<I, V> {
426        fn as_ref(&self) -> &MyopicTorus<I, V> {
427            &self.0
428        }
429    }
430
431    impl<I: ICompatible<V>, V: VCompatible<I>> From<MyopicTorus<I, V>> for MyopicTorusCover<I, V> {
432        fn from(value: MyopicTorus<I, V>) -> Self {
433            Self(value)
434        }
435    }
436
437    impl<I: ICompatible<V>, V: VCompatible<I>> Bounded<MyopicTorus<I, V>, Torus<I, V>, V>
438        for MyopicTorusCover<I, V>
439    {
440        fn sdf(&self, v: &V) -> <V as Tensor>::F {
441            let to = |x| <V::F as NumCast>::from(x).unwrap();
442            v.norm() - (to(2).sqrt() + to(2)) / to(4 * MyopicTorus::<I, V>::s())
443        }
444    }
445
446    impl_tangent_bundle_via_bounded!(
447        MyopicTorusCover<I, V>,
448        MyopicTorus<I, V>,
449        Torus<I, V>,
450        V,
451        I: ICompatible<V>, V: VCompatible<I>
452    );
453
454    impl<I: ICompatible<V>, V: VCompatible<I>> BuildNodes<MyopicTorusCover<I, V>>
455        for MyopicTorusCover<I, V>
456    {
457        fn build_nodes() -> Vec<Self> {
458            let to = |x| <I::F as NumCast>::from(x).unwrap();
459            let s_usize = MyopicTorus::<I, V>::s();
460            let s = to(s_usize);
461            let offset = to(1) / (to(2) * s);
462
463            (0..s_usize)
464                .flat_map(|y| (0..s_usize).map(move |x| (x, y)))
465                .map(|(x, y)| {
466                    MyopicTorus(Torus::new(
467                        S1([offset + to(x) / s].into()),
468                        S1([offset + to(y) / s].into()),
469                    ))
470                    .into()
471                })
472                .collect()
473        }
474    }
475
476    impl<I: ICompatible<V>, V: VCompatible<I>>
477        NerveComplexParameters<Torus<I, V>, V, MyopicTorus<I, V>, MyopicTorusCover<I, V>>
478        for MyopicTorusCover<I, V>
479    {
480        fn overestimation_bound() -> Option<(V::F, V::F)> {
481            let to = |x| <V::F as NumCast>::from(x).unwrap();
482            let s = to(MyopicTorus::<I, V>::s());
483            // κ: king-graph worst case at 22.5°, √(4 − 2√2). Scale-free.
484            let kappa = (to(4) - to(2) * to(2).sqrt()).sqrt();
485            // C = (1+κ)·2δ_s, with δ_s = √2/(2S) the lattice half-diagonal.
486            let delta_s = to(2).sqrt() / (to(2) * s);
487            Some((kappa, (V::F::one() + kappa) * to(2) * delta_s))
488        }
489    }
490}
491
492#[cfg(feature = "simplicial")]
493pub use simplicial::*;