Skip to main content

cartan_core/
bundle.rs

1//! Discrete fiber bundle connections and covariant Laplacian.
2//!
3//! A discrete connection stores SO(d) frame transport matrices per mesh edge.
4//! The covariant Laplacian uses these transports with cotangent weights to
5//! compute the connection Laplacian on any fiber bundle section.
6//!
7//! ## Sign convention
8//!
9//! The DEC Laplacian is **positive-semidefinite** (positive at maxima):
10//!
11//! ```text
12//! (Delta s)_v = (1/A_v) * sum_{e incident to v} w_e * (s_v - T_e(s_neighbor))
13//! ```
14//!
15//! Elastic smoothing in physical equations enters as **-K * Delta** (minus sign).
16
17#[cfg(feature = "alloc")]
18use alloc::{vec, vec::Vec};
19
20use crate::fiber::{Fiber, FiberOps, Section, VecSection};
21
22/// Discrete connection: SO(D) frame transport per edge.
23///
24/// Each edge stores a D x D orthogonal matrix (row-major flat slice of
25/// length D*D) representing parallel transport from vertex v0 to v1.
26/// The reverse transport (v1 to v0) is the matrix transpose.
27pub trait DiscreteConnection<const D: usize> {
28    /// Number of edges.
29    fn n_edges(&self) -> usize;
30
31    /// Edge endpoints: `[v0, v1]` for edge `e`.
32    fn edge_vertices(&self, e: usize) -> [usize; 2];
33
34    /// SO(D) transport matrix for edge `e` (v0 -> v1), row-major flat.
35    /// Returned slice has length >= D*D.
36    fn frame_transport(&self, e: usize) -> &[f64];
37
38    /// Transport a fiber element from v0 to v1 along edge `e`.
39    fn transport_forward<F: Fiber>(&self, e: usize, element: &F::Element) -> F::Element {
40        F::transport_by(self.frame_transport(e), D, element)
41    }
42
43    /// Transport a fiber element from v1 to v0 along edge `e` (uses R^T).
44    fn transport_reverse<F: Fiber>(&self, e: usize, element: &F::Element) -> F::Element {
45        let r = self.frame_transport(e);
46        let mut r_t = [0.0_f64; 9]; // max D=3 -> 9 entries
47        for i in 0..D {
48            for j in 0..D {
49                r_t[i * D + j] = r[j * D + i];
50            }
51        }
52        F::transport_by(&r_t[..D * D], D, element)
53    }
54}
55
56/// Edge-based transport storage for SO(2) (4 floats per edge).
57#[derive(Clone, Debug)]
58pub struct EdgeTransport2D {
59    /// Edge endpoints.
60    pub edges: Vec<[usize; 2]>,
61    /// SO(2) transport matrices, row-major: [cos, -sin, sin, cos] per edge.
62    pub transports: Vec<[f64; 4]>,
63}
64
65impl DiscreteConnection<2> for EdgeTransport2D {
66    fn n_edges(&self) -> usize { self.edges.len() }
67    fn edge_vertices(&self, e: usize) -> [usize; 2] { self.edges[e] }
68    fn frame_transport(&self, e: usize) -> &[f64] { &self.transports[e] }
69}
70
71/// Edge-based transport storage for SO(3) (9 floats per edge).
72#[derive(Clone, Debug)]
73pub struct EdgeTransport3D {
74    /// Edge endpoints.
75    pub edges: Vec<[usize; 2]>,
76    /// SO(3) transport matrices, row-major (9 floats per edge).
77    pub transports: Vec<[f64; 9]>,
78}
79
80impl DiscreteConnection<3> for EdgeTransport3D {
81    fn n_edges(&self) -> usize { self.edges.len() }
82    fn edge_vertices(&self, e: usize) -> [usize; 2] { self.edges[e] }
83    fn frame_transport(&self, e: usize) -> &[f64] { &self.transports[e] }
84}
85
86/// Generic covariant Laplacian on fiber bundle sections.
87///
88/// Applies the DEC connection Laplacian to a section of any fiber bundle:
89///
90/// ```text
91/// (Delta s)_v = (1/A_v) * sum_{e incident to v} w_e * (s_v - T_e(s_neighbor))
92/// ```
93///
94/// where T_e is the parallel transport from neighbor to v along edge e,
95/// w_e is the cotangent weight, and A_v is the dual cell area.
96///
97/// **Positive-semidefinite** (positive at maxima). Use `-K * lap` for smoothing.
98pub struct CovLaplacian {
99    /// Cotangent weights per edge.
100    cot_weights: Vec<f64>,
101    /// Dual cell area per vertex (star_0).
102    dual_areas: Vec<f64>,
103    /// Edge endpoints.
104    edges: Vec<[usize; 2]>,
105    /// For each vertex: list of (edge_idx, is_v0) pairs.
106    vertex_edges: Vec<Vec<(usize, bool)>>,
107}
108
109impl CovLaplacian {
110    /// Build the Laplacian stencil from mesh topology and weights.
111    ///
112    /// `n_vertices` is the total vertex count.
113    /// `edges` are `[v0, v1]` pairs (same order as the connection).
114    /// `cot_weights[e]` is the cotangent weight for edge e.
115    /// `dual_areas[v]` is the dual cell area (star_0) for vertex v.
116    pub fn new(
117        n_vertices: usize,
118        edges: &[[usize; 2]],
119        cot_weights: &[f64],
120        dual_areas: &[f64],
121    ) -> Self {
122        let mut vertex_edges: Vec<Vec<(usize, bool)>> = vec![vec![]; n_vertices];
123        for (e, &[v0, v1]) in edges.iter().enumerate() {
124            vertex_edges[v0].push((e, true));
125            vertex_edges[v1].push((e, false));
126        }
127        Self {
128            cot_weights: cot_weights.to_vec(),
129            dual_areas: dual_areas.to_vec(),
130            edges: edges.to_vec(),
131            vertex_edges,
132        }
133    }
134
135    /// Apply the covariant Laplacian with a discrete connection.
136    ///
137    /// Generic over fiber type `F` and connection dimension `D`.
138    pub fn apply<F, const D: usize, C>(
139        &self,
140        section: &impl Section<F>,
141        conn: &C,
142    ) -> VecSection<F>
143    where
144        F: FiberOps,
145        C: DiscreteConnection<D>,
146    {
147        let nv = section.n_vertices();
148        let mut result = VecSection::<F>::zeros(nv);
149
150        for v in 0..nv {
151            let s_v = section.at(v);
152
153            for &(e, is_v0) in &self.vertex_edges[v] {
154                let neighbor = if is_v0 { self.edges[e][1] } else { self.edges[e][0] };
155                let s_neighbor = section.at(neighbor);
156                let w = self.cot_weights[e];
157
158                // Transport neighbor's value to v's frame.
159                let transported = if is_v0 {
160                    conn.transport_reverse::<F>(e, s_neighbor)
161                } else {
162                    conn.transport_forward::<F>(e, s_neighbor)
163                };
164
165                // Accumulate w * (s_v - transported).
166                F::accumulate_diff(result.at_mut(v), s_v, &transported, w);
167            }
168
169            // Divide by dual area.
170            if self.dual_areas[v] > 1e-30 {
171                F::scale_element(result.at_mut(v), 1.0 / self.dual_areas[v]);
172            }
173        }
174
175        result
176    }
177}