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}