Skip to main content

oxiphysics_core/differential_geometry/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::types::{
7    GeodesicState, LeviCivitaConnection, MetricTensorND, OneForm, RicciTensor, RiemannTensor,
8    RiemannianMetric, SecondFundamentalForm, So3, TwoForm,
9};
10
11/// A 3-vector.
12pub type Vec3 = [f64; 3];
13/// A 3×3 matrix stored row-major.
14pub type Mat3 = [[f64; 3]; 3];
15/// A 4×4 matrix stored row-major.
16pub type Mat4 = [[f64; 4]; 4];
17/// A unit quaternion \[w, x, y, z\].
18pub type Quat = [f64; 4];
19/// Add two 3-vectors.
20pub fn add3(a: Vec3, b: Vec3) -> Vec3 {
21    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
22}
23/// Subtract two 3-vectors.
24pub fn sub3(a: Vec3, b: Vec3) -> Vec3 {
25    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27/// Scale a 3-vector.
28pub fn scale3(a: Vec3, s: f64) -> Vec3 {
29    [a[0] * s, a[1] * s, a[2] * s]
30}
31/// Dot product of two 3-vectors.
32pub fn dot3(a: Vec3, b: Vec3) -> f64 {
33    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
34}
35/// Cross product of two 3-vectors.
36pub fn cross3(a: Vec3, b: Vec3) -> Vec3 {
37    [
38        a[1] * b[2] - a[2] * b[1],
39        a[2] * b[0] - a[0] * b[2],
40        a[0] * b[1] - a[1] * b[0],
41    ]
42}
43/// Norm of a 3-vector.
44pub fn norm3(a: Vec3) -> f64 {
45    dot3(a, a).sqrt()
46}
47/// Normalize a 3-vector (returns zero vector if near zero).
48pub fn normalize3(a: Vec3) -> Vec3 {
49    let n = norm3(a);
50    if n < 1e-14 {
51        [0.0, 0.0, 0.0]
52    } else {
53        scale3(a, 1.0 / n)
54    }
55}
56/// 3×3 identity matrix.
57pub fn mat3_identity() -> Mat3 {
58    [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
59}
60/// 3×3 zero matrix.
61pub fn mat3_zero() -> Mat3 {
62    [[0.0; 3]; 3]
63}
64/// Matrix–vector multiply: M·v.
65pub fn mat3_mul_vec3(m: Mat3, v: Vec3) -> Vec3 {
66    [dot3(m[0], v), dot3(m[1], v), dot3(m[2], v)]
67}
68/// Matrix–matrix multiply: A·B.
69pub fn mat3_mul(a: Mat3, b: Mat3) -> Mat3 {
70    let bt = mat3_transpose(b);
71    [
72        [dot3(a[0], bt[0]), dot3(a[0], bt[1]), dot3(a[0], bt[2])],
73        [dot3(a[1], bt[0]), dot3(a[1], bt[1]), dot3(a[1], bt[2])],
74        [dot3(a[2], bt[0]), dot3(a[2], bt[1]), dot3(a[2], bt[2])],
75    ]
76}
77/// Matrix transpose.
78pub fn mat3_transpose(m: Mat3) -> Mat3 {
79    [
80        [m[0][0], m[1][0], m[2][0]],
81        [m[0][1], m[1][1], m[2][1]],
82        [m[0][2], m[1][2], m[2][2]],
83    ]
84}
85/// Matrix trace.
86pub fn mat3_trace(m: Mat3) -> f64 {
87    m[0][0] + m[1][1] + m[2][2]
88}
89/// Frobenius norm of a 3×3 matrix.
90pub fn mat3_frobenius_norm(m: Mat3) -> f64 {
91    let mut s = 0.0;
92    for row in &m {
93        for &v in row {
94            s += v * v;
95        }
96    }
97    s.sqrt()
98}
99/// Add two 3×3 matrices.
100pub fn mat3_add(a: Mat3, b: Mat3) -> Mat3 {
101    let mut r = mat3_zero();
102    for i in 0..3 {
103        for j in 0..3 {
104            r[i][j] = a[i][j] + b[i][j];
105        }
106    }
107    r
108}
109/// Scale a 3×3 matrix by scalar.
110pub fn mat3_scale(m: Mat3, s: f64) -> Mat3 {
111    let mut r = mat3_zero();
112    for i in 0..3 {
113        for j in 0..3 {
114            r[i][j] = m[i][j] * s;
115        }
116    }
117    r
118}
119/// 3×3 determinant.
120pub fn mat3_det(m: Mat3) -> f64 {
121    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
122        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
123        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
124}
125/// Skew-symmetric (hat) operator: converts a 3-vector ω to \[ω\]×.
126pub fn skew3(v: Vec3) -> Mat3 {
127    [[0.0, -v[2], v[1]], [v[2], 0.0, -v[0]], [-v[1], v[0], 0.0]]
128}
129/// Vee (inverse hat) operator: extracts 3-vector from skew-symmetric matrix.
130pub fn vee3(m: Mat3) -> Vec3 {
131    [m[2][1], m[0][2], m[1][0]]
132}
133/// Wedge product of two 1-forms: α ∧ β.
134pub fn wedge(alpha: OneForm, beta: OneForm) -> TwoForm {
135    let a = alpha.components;
136    let b = beta.components;
137    TwoForm {
138        dxdy: a[0] * b[1] - a[1] * b[0],
139        dydz: a[1] * b[2] - a[2] * b[1],
140        dzdx: a[2] * b[0] - a[0] * b[2],
141    }
142}
143/// Parallel transport of a vector along a curve on SO(3).
144///
145/// `initial_vec` – vector to be transported.
146/// `angular_velocity` – angular velocity of the frame along the curve (rad/s).
147/// `dt` – time step.
148///
149/// Returns the transported vector after one step.
150pub fn parallel_transport_step(initial_vec: Vec3, angular_velocity: Vec3, dt: f64) -> Vec3 {
151    let omega = scale3(angular_velocity, dt);
152    let rot = So3::exp(omega);
153    rot.rotate(initial_vec)
154}
155/// Parallel transport along a sequence of angular velocities.
156pub fn parallel_transport(initial_vec: Vec3, angular_velocities: &[Vec3], dt: f64) -> Vec3 {
157    let mut v = initial_vec;
158    for &omega in angular_velocities {
159        v = parallel_transport_step(v, omega, dt);
160    }
161    v
162}
163/// Riemannian exponential map on SO(3) at base point R₀.
164///
165/// `r0` – base point (rotation).
166/// `tangent` – tangent vector in T_{R₀}SO(3) (Lie algebra element).
167///
168/// Returns the geodesic endpoint.
169pub fn so3_exp_map(r0: &So3, tangent: Vec3) -> So3 {
170    let step = So3::exp(tangent);
171    r0.compose(&step)
172}
173/// Riemannian logarithm map on SO(3): log_{R₀}(R₁).
174///
175/// Returns the tangent vector pointing from R₀ to R₁ in T_{R₀}SO(3).
176pub fn so3_log_map(r0: &So3, r1: &So3) -> Vec3 {
177    let rel = r0.inverse().compose(r1);
178    rel.log()
179}
180/// Geodesic on SO(3) between two rotations, sampled at parameter t ∈ \[0,1\].
181pub fn so3_geodesic(r0: &So3, r1: &So3, t: f64) -> So3 {
182    r0.slerp(r1, t)
183}
184/// Numerically approximate Christoffel symbols Γ^k_ij for a surface
185/// defined by a mapping r(u, v) via finite differences.
186///
187/// `r_fn` – parametric surface function r(u, v) → R³.
188/// `u`, `v` – evaluation point.
189/// `h` – finite difference step size.
190///
191/// Returns Christoffel symbols as a 2×2×2 array: `gamma[k][i][j]`.
192pub fn christoffel_symbols_numerical<F>(r_fn: &F, u: f64, v: f64, h: f64) -> [[[f64; 2]; 2]; 2]
193where
194    F: Fn(f64, f64) -> Vec3,
195{
196    let ru = scale3(sub3(r_fn(u + h, v), r_fn(u - h, v)), 1.0 / (2.0 * h));
197    let rv = scale3(sub3(r_fn(u, v + h), r_fn(u, v - h)), 1.0 / (2.0 * h));
198    let r0 = r_fn(u, v);
199    let ruu = scale3(
200        sub3(add3(r_fn(u + h, v), r_fn(u - h, v)), scale3(r0, 2.0)),
201        1.0 / (h * h),
202    );
203    let rvv = scale3(
204        sub3(add3(r_fn(u, v + h), r_fn(u, v - h)), scale3(r0, 2.0)),
205        1.0 / (h * h),
206    );
207    let ruv = scale3(
208        sub3(
209            add3(r_fn(u + h, v + h), r_fn(u - h, v - h)),
210            add3(r_fn(u + h, v - h), r_fn(u - h, v + h)),
211        ),
212        1.0 / (4.0 * h * h),
213    );
214    let g = RiemannianMetric::from_tangents(ru, rv);
215    let (g11, g12, g22) = g.inverse();
216    let e_u = 2.0 * dot3(ruu, ru);
217    let e_v = 2.0 * dot3(ruv, ru);
218    let f_u = dot3(ruu, rv) + dot3(ruv, ru);
219    let f_v = dot3(ruv, rv) + dot3(rvv, ru);
220    let g_u = 2.0 * dot3(ruv, rv);
221    let g_v = 2.0 * dot3(rvv, rv);
222    let c111 = e_u / 2.0;
223    let c112 = e_v / 2.0;
224    let c121 = f_u - e_v / 2.0;
225    let c122 = g_u / 2.0;
226    let c221 = f_v - g_u / 2.0;
227    let c222 = g_v / 2.0;
228    let gamma_1_11 = g11 * c111 + g12 * c112;
229    let gamma_1_12 = g11 * c121 + g12 * c122;
230    let gamma_1_22 = g11 * c221 + g12 * c222;
231    let gamma_2_11 = g12 * c111 + g22 * c112;
232    let gamma_2_12 = g12 * c121 + g22 * c122;
233    let gamma_2_22 = g12 * c221 + g22 * c222;
234    [
235        [[gamma_1_11, gamma_1_12], [gamma_1_12, gamma_1_22]],
236        [[gamma_2_11, gamma_2_12], [gamma_2_12, gamma_2_22]],
237    ]
238}
239/// Integrate a geodesic on a surface by one step (Euler).
240///
241/// `state` – current geodesic state.
242/// `gamma` – Christoffel symbols at current parameter point.
243/// `ds` – arc-length step.
244pub fn geodesic_step(state: GeodesicState, gamma: [[[f64; 2]; 2]; 2], ds: f64) -> GeodesicState {
245    let u = state.u;
246    let v = state.v;
247    let du = state.du;
248    let dv = state.dv;
249    let ddu =
250        -(gamma[0][0][0] * du * du + 2.0 * gamma[0][0][1] * du * dv + gamma[0][1][1] * dv * dv);
251    let ddv =
252        -(gamma[1][0][0] * du * du + 2.0 * gamma[1][0][1] * du * dv + gamma[1][1][1] * dv * dv);
253    GeodesicState {
254        u: u + du * ds,
255        v: v + dv * ds,
256        du: du + ddu * ds,
257        dv: dv + ddv * ds,
258    }
259}
260/// Compute a geodesic curve on a surface given initial conditions.
261///
262/// Returns list of parameter pairs (u, v).
263pub fn geodesic_curve<F>(
264    r_fn: &F,
265    u0: f64,
266    v0: f64,
267    du0: f64,
268    dv0: f64,
269    num_steps: usize,
270    ds: f64,
271) -> Vec<(f64, f64)>
272where
273    F: Fn(f64, f64) -> Vec3,
274{
275    let mut state = GeodesicState::new(u0, v0, du0, dv0);
276    let mut curve = Vec::with_capacity(num_steps);
277    curve.push((state.u, state.v));
278    let h = ds * 1e-3;
279    for _ in 0..num_steps {
280        let gamma = christoffel_symbols_numerical(r_fn, state.u, state.v, h);
281        state = geodesic_step(state, gamma, ds);
282        curve.push((state.u, state.v));
283    }
284    curve
285}
286/// Numerically integrate Gaussian curvature over a patch of a surface.
287///
288/// Uses a simple uniform grid for integration.
289/// Returns the integral ∬K dA.
290pub fn gauss_bonnet_integral<F>(
291    r_fn: &F,
292    u_min: f64,
293    u_max: f64,
294    v_min: f64,
295    v_max: f64,
296    nu: usize,
297    nv: usize,
298) -> f64
299where
300    F: Fn(f64, f64) -> Vec3,
301{
302    let du = (u_max - u_min) / nu as f64;
303    let dv = (v_max - v_min) / nv as f64;
304    let h = du.min(dv) * 1e-3;
305    let mut integral = 0.0;
306    for i in 0..nu {
307        for j in 0..nv {
308            let u = u_min + (i as f64 + 0.5) * du;
309            let v = v_min + (j as f64 + 0.5) * dv;
310            let ru = scale3(sub3(r_fn(u + h, v), r_fn(u - h, v)), 0.5 / h);
311            let rv = scale3(sub3(r_fn(u, v + h), r_fn(u, v - h)), 0.5 / h);
312            let normal_vec = cross3(ru, rv);
313            let area = norm3(normal_vec);
314            if area < 1e-15 {
315                continue;
316            }
317            let unit_normal = scale3(normal_vec, 1.0 / area);
318            let r0 = r_fn(u, v);
319            let ruu = scale3(
320                sub3(add3(r_fn(u + h, v), r_fn(u - h, v)), scale3(r0, 2.0)),
321                1.0 / (h * h),
322            );
323            let rvv = scale3(
324                sub3(add3(r_fn(u, v + h), r_fn(u, v - h)), scale3(r0, 2.0)),
325                1.0 / (h * h),
326            );
327            let ruv = scale3(
328                sub3(
329                    add3(r_fn(u + h, v + h), r_fn(u - h, v - h)),
330                    add3(r_fn(u + h, v - h), r_fn(u - h, v + h)),
331                ),
332                0.25 / (h * h),
333            );
334            let g_metric = RiemannianMetric::from_tangents(ru, rv);
335            let sff = SecondFundamentalForm::from_derivatives(ruu, ruv, rvv, unit_normal);
336            let k = sff.gaussian_curvature(&g_metric);
337            integral += k * area * du * dv;
338        }
339    }
340    integral
341}
342/// Compute matrix exponential of a 3×3 matrix via Cayley-Hamilton theorem.
343///
344/// For a general 3×3 matrix A, exp(A) = c₀I + c₁A + c₂A².
345/// Uses eigenvalue decomposition for numerical stability.
346pub fn mat3_exp(a: Mat3) -> Mat3 {
347    let norm = mat3_frobenius_norm(a);
348    if norm < 1e-12 {
349        return mat3_identity();
350    }
351    let s = (norm / (2.0_f64.ln())).ceil().max(0.0) as u32;
352    let scale = 1.0 / (2_u64.pow(s) as f64);
353    let a_scaled = mat3_scale(a, scale);
354    let a2 = mat3_mul(a_scaled, a_scaled);
355    let a4 = mat3_mul(a2, a2);
356    let a6 = mat3_mul(a4, a2);
357    let b = [1.0 / 720.0, 1.0 / 120.0, 1.0 / 24.0, 1.0 / 6.0, 0.5, 1.0];
358    let i = mat3_identity();
359    let u = mat3_add(
360        mat3_add(
361            mat3_scale(a6, b[0] * scale.powi(6)),
362            mat3_scale(a4, b[2] * scale.powi(4)),
363        ),
364        mat3_add(mat3_scale(a2, b[4] * scale.powi(2)), mat3_scale(i, 1.0)),
365    );
366    let v = mat3_add(
367        mat3_add(
368            mat3_scale(a6, b[1] * scale.powi(5)),
369            mat3_scale(a4, b[3] * scale.powi(3)),
370        ),
371        mat3_add(mat3_scale(a2, b[5] * scale.powi(1)), i),
372    );
373    let _ = (u, v, b);
374    let mut result = mat3_identity();
375    let mut term = mat3_identity();
376    for k in 1..=10u32 {
377        term = mat3_scale(mat3_mul(term, a_scaled), 1.0 / k as f64);
378        result = mat3_add(result, term);
379    }
380    let mut r = result;
381    for _ in 0..s {
382        r = mat3_mul(r, r);
383    }
384    r
385}
386/// Compute matrix logarithm of a rotation matrix (SO(3)) via Rodrigues formula.
387pub fn so3_log_matrix(r: Mat3) -> Mat3 {
388    let trace = mat3_trace(r);
389    let cos_theta = ((trace - 1.0) / 2.0).clamp(-1.0, 1.0);
390    let theta = cos_theta.acos();
391    if theta.abs() < 1e-12 {
392        return mat3_zero();
393    }
394    let rt = mat3_transpose(r);
395
396    mat3_scale(
397        mat3_add(r, mat3_scale(rt, -1.0)),
398        theta / (2.0 * theta.sin()),
399    )
400}
401/// Lie bracket \[A, B\] = AB - BA for 3×3 matrices (elements of gl(3)).
402pub fn lie_bracket_mat3(a: Mat3, b: Mat3) -> Mat3 {
403    let ab = mat3_mul(a, b);
404    let ba = mat3_mul(b, a);
405    mat3_add(ab, mat3_scale(ba, -1.0))
406}
407/// Lie bracket for so(3) elements (skew-symmetric matrices): \[ω̂₁, ω̂₂\] = ω̂₁×ω̂₂ hat.
408///
409/// Equivalently: \[ω₁\]× × \[ω₂\]× = \[ω₁ × ω₂\]×.
410pub fn so3_lie_bracket(omega1: Vec3, omega2: Vec3) -> Vec3 {
411    cross3(omega1, omega2)
412}
413/// Adjoint action of SO(3) on so(3): Ad_R(ω) = R ω.
414pub fn so3_adjoint(r: &So3, omega: Vec3) -> Vec3 {
415    r.rotate(omega)
416}
417/// Coadjoint action of SO(3) on so(3)*: Ad*_R(μ) = R⁻ᵀ μ = R μ.
418pub fn so3_coadjoint(r: &So3, mu: Vec3) -> Vec3 {
419    r.rotate(mu)
420}
421/// Small adjoint (ad): ad_ω₁(ω₂) = ω₁ × ω₂.
422pub fn so3_small_adjoint(omega1: Vec3, omega2: Vec3) -> Vec3 {
423    cross3(omega1, omega2)
424}
425/// Maximum dimension supported for general metric tensor operations.
426pub const MAX_DIM: usize = 4;
427/// Ricci scalar R = g^{mu nu} R_{mu nu}.
428pub fn ricci_scalar(metric: &MetricTensorND, ricci: &RicciTensor) -> f64 {
429    let ginv = metric.inverse();
430    let dim = metric.dim;
431    let mut scalar = 0.0;
432    for mu in 0..dim {
433        for nu in 0..dim {
434            scalar += ginv[mu][nu] * ricci.components[mu][nu];
435        }
436    }
437    scalar
438}
439/// Compute the Ricci scalar directly from a metric function at a point.
440pub fn ricci_scalar_from_metric_fn<F>(
441    metric_fn: &F,
442    point: &[f64; MAX_DIM],
443    dim: usize,
444    h: f64,
445) -> f64
446where
447    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
448{
449    let ricci = RicciTensor::from_metric_fn(metric_fn, point, dim, h);
450    let g_here = metric_fn(point);
451    let metric = MetricTensorND { dim, g: g_here };
452    ricci_scalar(&metric, &ricci)
453}
454/// Einstein tensor G_{mu nu} = R_{mu nu} - 0.5 g_{mu nu} R.
455pub fn einstein_tensor(metric: &MetricTensorND, ricci: &RicciTensor) -> [[f64; MAX_DIM]; MAX_DIM] {
456    let r = ricci_scalar(metric, ricci);
457    let dim = metric.dim;
458    let mut g_tensor = [[0.0_f64; MAX_DIM]; MAX_DIM];
459    for mu in 0..dim {
460        for nu in 0..dim {
461            g_tensor[mu][nu] = ricci.components[mu][nu] - 0.5 * metric.g[mu][nu] * r;
462        }
463    }
464    g_tensor
465}
466/// Kretschner scalar K = R_{abcd} R^{abcd} (fully contracted Riemann tensor).
467pub fn kretschner_scalar<F>(metric_fn: &F, point: &[f64; MAX_DIM], dim: usize, h: f64) -> f64
468where
469    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
470{
471    let riemann = RiemannTensor::from_metric_fn(metric_fn, point, dim, h);
472    let g_here = metric_fn(point);
473    let metric = MetricTensorND { dim, g: g_here };
474    let ginv = metric.inverse();
475    let mut r_lower = [[[[0.0_f64; MAX_DIM]; MAX_DIM]; MAX_DIM]; MAX_DIM];
476    for a in 0..dim {
477        for b in 0..dim {
478            for c in 0..dim {
479                for d in 0..dim {
480                    let mut val = 0.0;
481                    for e in 0..dim {
482                        val += g_here[a][e] * riemann.components[e][b][c][d];
483                    }
484                    r_lower[a][b][c][d] = val;
485                }
486            }
487        }
488    }
489    let mut kretschner = 0.0;
490    for a in 0..dim {
491        for b in 0..dim {
492            for c in 0..dim {
493                for d in 0..dim {
494                    let mut r_upper = 0.0;
495                    for e in 0..dim {
496                        for f in 0..dim {
497                            for gg in 0..dim {
498                                for hh in 0..dim {
499                                    r_upper += ginv[a][e]
500                                        * ginv[b][f]
501                                        * ginv[c][gg]
502                                        * ginv[d][hh]
503                                        * r_lower[e][f][gg][hh];
504                                }
505                            }
506                        }
507                    }
508                    kretschner += r_lower[a][b][c][d] * r_upper;
509                }
510            }
511        }
512    }
513    kretschner
514}
515/// Covariant derivative of a vector field.
516///
517/// Given V^mu and its partial derivatives dV^mu/dx^nu at a point,
518/// returns (nabla_nu V)^mu = dV^mu/dx^nu + Gamma^mu_{nu lambda} V^lambda.
519pub fn covariant_derivative_vector(
520    conn: &LeviCivitaConnection,
521    v: &[f64; MAX_DIM],
522    dv: &[[f64; MAX_DIM]; MAX_DIM],
523) -> [[f64; MAX_DIM]; MAX_DIM] {
524    let dim = conn.dim;
525    let mut result = [[0.0_f64; MAX_DIM]; MAX_DIM];
526    for mu in 0..dim {
527        for nu in 0..dim {
528            let mut val = dv[mu][nu];
529            for lambda in 0..dim {
530                val += conn.christoffel[mu][nu][lambda] * v[lambda];
531            }
532            result[mu][nu] = val;
533        }
534    }
535    result
536}
537/// Covariant derivative of a covector (1-form) field.
538///
539/// Given omega_mu and its partial derivatives d omega_mu/dx^nu,
540/// returns (nabla_nu omega)_mu = d omega_mu/dx^nu - Gamma^lambda_{nu mu} omega_lambda.
541pub fn covariant_derivative_covector(
542    conn: &LeviCivitaConnection,
543    omega: &[f64; MAX_DIM],
544    domega: &[[f64; MAX_DIM]; MAX_DIM],
545) -> [[f64; MAX_DIM]; MAX_DIM] {
546    let dim = conn.dim;
547    let mut result = [[0.0_f64; MAX_DIM]; MAX_DIM];
548    for mu in 0..dim {
549        for nu in 0..dim {
550            let mut val = domega[mu][nu];
551            for lambda in 0..dim {
552                val -= conn.christoffel[lambda][nu][mu] * omega[lambda];
553            }
554            result[mu][nu] = val;
555        }
556    }
557    result
558}
559/// Solve the geodesic equation in N dimensions.
560///
561/// x''^\mu + Gamma^\mu_{\alpha\beta} x'^\alpha x'^\beta = 0
562///
563/// Uses RK4 integration.
564///
565/// Returns a vector of (position, velocity) pairs along the geodesic.
566#[allow(clippy::too_many_arguments)]
567pub fn geodesic_equation_solve<F>(
568    metric_fn: &F,
569    dim: usize,
570    x0: &[f64; MAX_DIM],
571    v0: &[f64; MAX_DIM],
572    dt: f64,
573    steps: usize,
574    h_christoffel: f64,
575) -> Vec<([f64; MAX_DIM], [f64; MAX_DIM])>
576where
577    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
578{
579    let mut trajectory = Vec::with_capacity(steps + 1);
580    let mut x = *x0;
581    let mut v = *v0;
582    trajectory.push((x, v));
583    for _ in 0..steps {
584        let accel = |pos: &[f64; MAX_DIM], vel: &[f64; MAX_DIM]| -> [f64; MAX_DIM] {
585            let conn = LeviCivitaConnection::from_metric_fn(metric_fn, pos, dim, h_christoffel);
586            let mut a = [0.0; MAX_DIM];
587            for mu in 0..dim {
588                let mut val = 0.0;
589                for alpha in 0..dim {
590                    for beta in 0..dim {
591                        val += conn.christoffel[mu][alpha][beta] * vel[alpha] * vel[beta];
592                    }
593                }
594                a[mu] = -val;
595            }
596            a
597        };
598        let a1 = accel(&x, &v);
599        let mut x1 = [0.0; MAX_DIM];
600        let mut v1 = [0.0; MAX_DIM];
601        for i in 0..dim {
602            x1[i] = x[i] + 0.5 * dt * v[i];
603            v1[i] = v[i] + 0.5 * dt * a1[i];
604        }
605        let a2 = accel(&x1, &v1);
606        let mut x2 = [0.0; MAX_DIM];
607        let mut v2 = [0.0; MAX_DIM];
608        for i in 0..dim {
609            x2[i] = x[i] + 0.5 * dt * v1[i];
610            v2[i] = v[i] + 0.5 * dt * a2[i];
611        }
612        let a3 = accel(&x2, &v2);
613        let mut x3 = [0.0; MAX_DIM];
614        let mut v3 = [0.0; MAX_DIM];
615        for i in 0..dim {
616            x3[i] = x[i] + dt * v2[i];
617            v3[i] = v[i] + dt * a3[i];
618        }
619        let a4 = accel(&x3, &v3);
620        for i in 0..dim {
621            x[i] += dt / 6.0 * (v[i] + 2.0 * v1[i] + 2.0 * v2[i] + v3[i]);
622            v[i] += dt / 6.0 * (a1[i] + 2.0 * a2[i] + 2.0 * a3[i] + a4[i]);
623        }
624        trajectory.push((x, v));
625    }
626    trajectory
627}
628/// Parallel transport a vector along a curve in N dimensions.
629///
630/// `curve` is a sequence of coordinate points.
631/// `v0` is the initial vector to transport.
632/// `metric_fn` returns the metric at any point.
633///
634/// Returns the transported vector at each point of the curve.
635pub fn parallel_transport_nd<F>(
636    metric_fn: &F,
637    dim: usize,
638    curve: &[[f64; MAX_DIM]],
639    v0: &[f64; MAX_DIM],
640    h_christoffel: f64,
641) -> Vec<[f64; MAX_DIM]>
642where
643    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
644{
645    let mut result = Vec::with_capacity(curve.len());
646    let mut v = *v0;
647    result.push(v);
648    for i in 1..curve.len() {
649        let conn =
650            LeviCivitaConnection::from_metric_fn(metric_fn, &curve[i - 1], dim, h_christoffel);
651        let mut dx = [0.0; MAX_DIM];
652        for k in 0..dim {
653            dx[k] = curve[i][k] - curve[i - 1][k];
654        }
655        let mut dv = [0.0; MAX_DIM];
656        for mu in 0..dim {
657            for alpha in 0..dim {
658                for beta in 0..dim {
659                    dv[mu] -= conn.christoffel[mu][alpha][beta] * v[alpha] * dx[beta];
660                }
661            }
662        }
663        for mu in 0..dim {
664            v[mu] += dv[mu];
665        }
666        result.push(v);
667    }
668    result
669}
670/// Check if a vector field is a Killing vector field.
671///
672/// A Killing vector field satisfies: nabla_mu xi_nu + nabla_nu xi_mu = 0.
673///
674/// `xi_fn` returns the Killing vector at a point.
675/// Returns the maximum violation of the Killing equation.
676#[allow(clippy::too_many_arguments)]
677pub fn killing_equation_violation<F, G>(
678    metric_fn: &F,
679    xi_fn: &G,
680    point: &[f64; MAX_DIM],
681    dim: usize,
682    h: f64,
683) -> f64
684where
685    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
686    G: Fn(&[f64; MAX_DIM]) -> [f64; MAX_DIM],
687{
688    let conn = LeviCivitaConnection::from_metric_fn(metric_fn, point, dim, h);
689    let g_here = metric_fn(point);
690    let xi = xi_fn(point);
691    let mut xi_lower = [0.0; MAX_DIM];
692    for mu in 0..dim {
693        for nu in 0..dim {
694            xi_lower[mu] += g_here[mu][nu] * xi[nu];
695        }
696    }
697    let mut dxi_lower = [[0.0_f64; MAX_DIM]; MAX_DIM];
698    for nu in 0..dim {
699        let mut p_plus = *point;
700        let mut p_minus = *point;
701        p_plus[nu] += h;
702        p_minus[nu] -= h;
703        let xi_plus = xi_fn(&p_plus);
704        let xi_minus = xi_fn(&p_minus);
705        let g_plus = metric_fn(&p_plus);
706        let g_minus = metric_fn(&p_minus);
707        for mu in 0..dim {
708            let mut xi_low_plus = 0.0;
709            let mut xi_low_minus = 0.0;
710            for rho in 0..dim {
711                xi_low_plus += g_plus[mu][rho] * xi_plus[rho];
712                xi_low_minus += g_minus[mu][rho] * xi_minus[rho];
713            }
714            dxi_lower[mu][nu] = (xi_low_plus - xi_low_minus) / (2.0 * h);
715        }
716    }
717    let nabla_xi = covariant_derivative_covector(&conn, &xi_lower, &dxi_lower);
718    let mut max_viol = 0.0_f64;
719    for mu in 0..dim {
720        for nu in 0..dim {
721            let viol = (nabla_xi[nu][mu] + nabla_xi[mu][nu]).abs();
722            max_viol = max_viol.max(viol);
723        }
724    }
725    max_viol
726}
727/// Weyl tensor C^rho_{sigma mu nu} for dim >= 3.
728///
729/// C = R - (2/(n-2))(g * Ric - g * g * R/(n-1)) where * denotes Kulkarni-Nomizu product
730/// Simplified direct formula:
731/// C^rho_{sigma mu nu} = R^rho_{sigma mu nu}
732///   - 1/(n-2) (delta^rho_mu R_{sigma nu} - delta^rho_nu R_{sigma mu}
733///     + g_{sigma nu} R^rho_mu - g_{sigma mu} R^rho_nu)
734///   + R/((n-1)(n-2)) (delta^rho_mu g_{sigma nu} - delta^rho_nu g_{sigma mu})
735pub fn weyl_tensor<F>(
736    metric_fn: &F,
737    point: &[f64; MAX_DIM],
738    dim: usize,
739    h: f64,
740) -> [[[[f64; MAX_DIM]; MAX_DIM]; MAX_DIM]; MAX_DIM]
741where
742    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
743{
744    assert!(dim >= 3, "Weyl tensor only defined for dim >= 3");
745    let riemann = RiemannTensor::from_metric_fn(metric_fn, point, dim, h);
746    let ricci = RicciTensor::from_riemann(&riemann);
747    let g_here = metric_fn(point);
748    let metric = MetricTensorND { dim, g: g_here };
749    let ginv = metric.inverse();
750    let r = ricci_scalar(&metric, &ricci);
751    let mut ricci_mixed = [[0.0_f64; MAX_DIM]; MAX_DIM];
752    for rho in 0..dim {
753        for mu in 0..dim {
754            for sigma in 0..dim {
755                ricci_mixed[rho][mu] += ginv[rho][sigma] * ricci.components[sigma][mu];
756            }
757        }
758    }
759    let n = dim as f64;
760    let mut weyl = [[[[0.0_f64; MAX_DIM]; MAX_DIM]; MAX_DIM]; MAX_DIM];
761    let delta = |i: usize, j: usize| -> f64 { if i == j { 1.0 } else { 0.0 } };
762    for rho in 0..dim {
763        for sigma in 0..dim {
764            for mu in 0..dim {
765                for nu in 0..dim {
766                    let ricci_term = delta(rho, mu) * ricci.components[sigma][nu]
767                        - delta(rho, nu) * ricci.components[sigma][mu]
768                        + g_here[sigma][nu] * ricci_mixed[rho][mu]
769                        - g_here[sigma][mu] * ricci_mixed[rho][nu];
770                    let scalar_term = r
771                        * (delta(rho, mu) * g_here[sigma][nu] - delta(rho, nu) * g_here[sigma][mu]);
772                    weyl[rho][sigma][mu][nu] = riemann.components[rho][sigma][mu][nu]
773                        - ricci_term / (n - 2.0)
774                        + scalar_term / ((n - 1.0) * (n - 2.0));
775                }
776            }
777        }
778    }
779    weyl
780}
781/// Geodesic deviation equation (Jacobi equation).
782///
783/// Given a geodesic (x(t), v(t)) and an initial deviation vector xi(0) and its derivative,
784/// computes the evolution of the deviation vector.
785///
786/// D^2 xi^mu / dt^2 = -R^mu_{alpha beta gamma} v^alpha xi^beta v^gamma
787#[allow(clippy::too_many_arguments)]
788pub fn geodesic_deviation<F>(
789    metric_fn: &F,
790    dim: usize,
791    geodesic: &[([f64; MAX_DIM], [f64; MAX_DIM])],
792    xi0: &[f64; MAX_DIM],
793    dxi0: &[f64; MAX_DIM],
794    dt: f64,
795    h: f64,
796) -> Vec<[f64; MAX_DIM]>
797where
798    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
799{
800    let mut result = Vec::with_capacity(geodesic.len());
801    let mut xi = *xi0;
802    let mut dxi = *dxi0;
803    result.push(xi);
804    for i in 1..geodesic.len() {
805        let (pos, vel) = &geodesic[i - 1];
806        let riemann = RiemannTensor::from_metric_fn(metric_fn, pos, dim, h);
807        let mut ddxi = [0.0; MAX_DIM];
808        for mu in 0..dim {
809            for alpha in 0..dim {
810                for beta in 0..dim {
811                    for gamma in 0..dim {
812                        ddxi[mu] -= riemann.components[mu][alpha][beta][gamma]
813                            * vel[alpha]
814                            * xi[beta]
815                            * vel[gamma];
816                    }
817                }
818            }
819        }
820        for mu in 0..dim {
821            xi[mu] += dxi[mu] * dt;
822            dxi[mu] += ddxi[mu] * dt;
823        }
824        result.push(xi);
825    }
826    result
827}
828/// Compute the Gauss-Bonnet integrand in N=2 dimensions.
829///
830/// For a 2D surface with metric g_{ij}, the Gauss-Bonnet theorem states:
831/// integral(K * sqrt(det g) du dv) = 2*pi*chi
832///
833/// where K is the Gaussian curvature and chi is the Euler characteristic.
834pub fn gauss_bonnet_2d_integrand<F>(metric_fn: &F, point: &[f64; MAX_DIM], h: f64) -> f64
835where
836    F: Fn(&[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM],
837{
838    let g_here = metric_fn(point);
839    let metric = MetricTensorND { dim: 2, g: g_here };
840    let r = ricci_scalar_from_metric_fn(metric_fn, point, 2, h);
841    let k = r / 2.0;
842    k * metric.volume_element()
843}
844#[cfg(test)]
845mod tests {
846    use super::*;
847    use crate::differential_geometry::Se3;
848    use crate::differential_geometry::Se3Algebra;
849    use std::f64::consts::PI;
850    pub(super) const EPS: f64 = 1e-10;
851    #[test]
852    fn test_so3_identity_exp_zero() {
853        let r = So3::exp([0.0, 0.0, 0.0]);
854        for i in 0..3 {
855            for j in 0..3 {
856                let expected = if i == j { 1.0 } else { 0.0 };
857                assert!((r.mat[i][j] - expected).abs() < EPS);
858            }
859        }
860    }
861    #[test]
862    fn test_so3_exp_log_roundtrip() {
863        let omega = [0.3, -0.5, 0.2];
864        let r = So3::exp(omega);
865        let omega2 = r.log();
866        for i in 0..3 {
867            assert!(
868                (omega[i] - omega2[i]).abs() < 1e-10,
869                "Component {} mismatch",
870                i
871            );
872        }
873    }
874    #[test]
875    fn test_so3_orthogonality() {
876        let r = So3::exp([0.5, -0.3, 0.7]);
877        let rt = mat3_transpose(r.mat);
878        let rrt = mat3_mul(r.mat, rt);
879        let i = mat3_identity();
880        for row in 0..3 {
881            for col in 0..3 {
882                assert!((rrt[row][col] - i[row][col]).abs() < 1e-10);
883            }
884        }
885    }
886    #[test]
887    fn test_so3_determinant_one() {
888        let r = So3::exp([1.0, 0.5, -0.3]);
889        let d = mat3_det(r.mat);
890        assert!((d - 1.0).abs() < 1e-10);
891    }
892    #[test]
893    fn test_so3_inverse_compose_identity() {
894        let r = So3::exp([0.4, -0.2, 0.8]);
895        let r_inv = r.inverse();
896        let prod = r.compose(&r_inv);
897        let omega = prod.log();
898        assert!(norm3(omega) < 1e-10);
899    }
900    #[test]
901    fn test_so3_slerp_endpoints() {
902        let r0 = So3::identity();
903        let r1 = So3::exp([0.3, 0.1, -0.2]);
904        let s0 = r0.slerp(&r1, 0.0);
905        let omega_s0 = s0.log();
906        assert!(norm3(omega_s0) < 1e-10);
907    }
908    #[test]
909    fn test_so3_slerp_midpoint() {
910        let r0 = So3::identity();
911        let omega = [0.6, 0.0, 0.0];
912        let r1 = So3::exp(omega);
913        let mid = r0.slerp(&r1, 0.5);
914        let log_mid = mid.log();
915        assert!((log_mid[0] - 0.3).abs() < 1e-10);
916    }
917    #[test]
918    fn test_so3_quaternion_roundtrip() {
919        let r = So3::exp([0.5, -0.3, 0.1]);
920        let q = r.to_quaternion();
921        let r2 = So3::from_quaternion(q);
922        for i in 0..3 {
923            for j in 0..3 {
924                assert!((r.mat[i][j] - r2.mat[i][j]).abs() < 1e-10);
925            }
926        }
927    }
928    #[test]
929    fn test_so3_geodesic_distance_zero_to_self() {
930        let r = So3::exp([0.3, -0.1, 0.5]);
931        assert!(r.dist(&r) < 1e-10);
932    }
933    #[test]
934    fn test_so3_adjoint_equals_rotation() {
935        let r = So3::exp([0.2, 0.3, -0.1]);
936        let v = [1.0, 0.0, 0.0];
937        let adj = so3_adjoint(&r, v);
938        let rot = r.rotate(v);
939        for i in 0..3 {
940            assert!((adj[i] - rot[i]).abs() < 1e-10);
941        }
942    }
943    #[test]
944    fn test_se3_identity() {
945        let id = Se3::identity();
946        let p = [1.0, 2.0, 3.0];
947        let t = id.transform_point(p);
948        for i in 0..3 {
949            assert!((t[i] - p[i]).abs() < 1e-10);
950        }
951    }
952    #[test]
953    fn test_se3_exp_log_roundtrip() {
954        let twist = Se3Algebra::new([0.1, -0.2, 0.3], [1.0, -0.5, 0.2]);
955        let se3 = Se3::exp(twist);
956        let twist2 = se3.log();
957        for i in 0..3 {
958            assert!((twist.omega[i] - twist2.omega[i]).abs() < 1e-8);
959        }
960    }
961    #[test]
962    fn test_se3_inverse() {
963        let se3 = Se3::from_rt(So3::exp([0.3, 0.1, -0.2]), [1.0, 2.0, 3.0]);
964        let inv = se3.inverse();
965        let composed = se3.compose(&inv);
966        let t = composed.translation;
967        for i in 0..3 {
968            assert!(t[i].abs() < 1e-10);
969        }
970    }
971    #[test]
972    fn test_se3_compose_translation() {
973        let t1 = Se3::from_rt(So3::identity(), [1.0, 0.0, 0.0]);
974        let t2 = Se3::from_rt(So3::identity(), [2.0, 0.0, 0.0]);
975        let t12 = t1.compose(&t2);
976        assert!((t12.translation[0] - 3.0).abs() < 1e-10);
977    }
978    #[test]
979    fn test_se3_mat4_last_row() {
980        let se3 = Se3::from_rt(So3::exp([0.1, 0.2, -0.3]), [5.0, -2.0, 1.0]);
981        let m = se3.to_mat4();
982        assert!((m[3][0]).abs() < 1e-10);
983        assert!((m[3][1]).abs() < 1e-10);
984        assert!((m[3][2]).abs() < 1e-10);
985        assert!((m[3][3] - 1.0).abs() < 1e-10);
986    }
987    #[test]
988    fn test_metric_sphere_area_element() {
989        let r = 2.0_f64;
990        let theta = std::f64::consts::PI / 2.0;
991        let g = RiemannianMetric::new(r * r, 0.0, r * r * theta.sin() * theta.sin());
992        let area = g.area_element();
993        assert!((area - r * r).abs() < 1e-10);
994    }
995    #[test]
996    fn test_metric_flat_plane() {
997        let g = RiemannianMetric::new(1.0, 0.0, 1.0);
998        assert!((g.det() - 1.0).abs() < 1e-10);
999    }
1000    #[test]
1001    fn test_metric_length_orthogonal() {
1002        let g = RiemannianMetric::new(4.0, 0.0, 9.0);
1003        assert!((g.length(1.0, 0.0) - 2.0).abs() < 1e-10);
1004        assert!((g.length(0.0, 1.0) - 3.0).abs() < 1e-10);
1005    }
1006    #[test]
1007    fn test_gauss_curvature_sphere() {
1008        let r = 3.0_f64;
1009        let ru = [r, 0.0, 0.0];
1010        let rv = [0.0, r, 0.0];
1011        let ruu = [-r, 0.0, 0.0];
1012        let ruv = [0.0, 0.0, 0.0];
1013        let rvv = [0.0, -r, 0.0];
1014        let n = [0.0, 0.0, 1.0];
1015        let g = RiemannianMetric::from_tangents(ru, rv);
1016        let sff = SecondFundamentalForm::from_derivatives(ruu, ruv, rvv, n);
1017        let k = sff.gaussian_curvature(&g);
1018        let expected = (sff.l * sff.n - sff.m * sff.m) / g.det();
1019        assert!((k - expected).abs() < 1e-10);
1020    }
1021    #[test]
1022    fn test_mean_curvature_flat_surface() {
1023        let g = RiemannianMetric::new(1.0, 0.0, 1.0);
1024        let sff = SecondFundamentalForm {
1025            l: 0.0,
1026            m: 0.0,
1027            n: 0.0,
1028        };
1029        assert_eq!(sff.mean_curvature(&g), 0.0);
1030    }
1031    #[test]
1032    fn test_one_form_evaluation() {
1033        let alpha = OneForm::new([1.0, 2.0, 3.0]);
1034        let v = [4.0, 5.0, 6.0];
1035        assert!((alpha.evaluate(v) - 32.0).abs() < 1e-10);
1036    }
1037    #[test]
1038    fn test_two_form_evaluation_antisymmetry() {
1039        let alpha = OneForm::new([1.0, 0.0, 0.0]);
1040        let beta = OneForm::new([0.0, 1.0, 0.0]);
1041        let form = wedge(alpha, beta);
1042        let u = [1.0, 0.0, 0.0];
1043        let v = [0.0, 1.0, 0.0];
1044        let val1 = form.evaluate(u, v);
1045        let val2 = form.evaluate(v, u);
1046        assert!((val1 + val2).abs() < 1e-10);
1047    }
1048    #[test]
1049    fn test_wedge_product_basis() {
1050        let ex = OneForm::new([1.0, 0.0, 0.0]);
1051        let ey = OneForm::new([0.0, 1.0, 0.0]);
1052        let form = wedge(ex, ey);
1053        assert!((form.dxdy - 1.0).abs() < 1e-10);
1054        assert!((form.dydz).abs() < 1e-10);
1055        assert!((form.dzdx).abs() < 1e-10);
1056    }
1057    #[test]
1058    fn test_parallel_transport_zero_rotation_identity() {
1059        let v = [1.0, 0.0, 0.0];
1060        let transported = parallel_transport(v, &[[0.0, 0.0, 0.0]; 10], 0.01);
1061        for i in 0..3 {
1062            assert!((transported[i] - v[i]).abs() < 1e-10);
1063        }
1064    }
1065    #[test]
1066    fn test_parallel_transport_preserves_length() {
1067        let v = [1.0, 2.0, 3.0];
1068        let init_len = norm3(v);
1069        let omegas: Vec<Vec3> = (0..20).map(|i| [0.01 * i as f64 * 0.1, 0.0, 0.0]).collect();
1070        let transported = parallel_transport(v, &omegas, 0.01);
1071        let final_len = norm3(transported);
1072        assert!((final_len - init_len).abs() < 1e-8);
1073    }
1074    #[test]
1075    fn test_mat3_exp_identity_at_zero() {
1076        let a = mat3_zero();
1077        let e = mat3_exp(a);
1078        let i = mat3_identity();
1079        for row in 0..3 {
1080            for col in 0..3 {
1081                assert!((e[row][col] - i[row][col]).abs() < 1e-10);
1082            }
1083        }
1084    }
1085    #[test]
1086    fn test_mat3_exp_so3_agrees() {
1087        let omega = [0.3, -0.2, 0.1];
1088        let r_rodrigues = So3::exp(omega);
1089        let skew = skew3(omega);
1090        let r_exp = mat3_exp(skew);
1091        for i in 0..3 {
1092            for j in 0..3 {
1093                assert!((r_rodrigues.mat[i][j] - r_exp[i][j]).abs() < 1e-8);
1094            }
1095        }
1096    }
1097    #[test]
1098    fn test_lie_bracket_antisymmetric() {
1099        let a = skew3([1.0, 0.0, 0.0]);
1100        let b = skew3([0.0, 1.0, 0.0]);
1101        let ab = lie_bracket_mat3(a, b);
1102        let ba = lie_bracket_mat3(b, a);
1103        for i in 0..3 {
1104            for j in 0..3 {
1105                assert!((ab[i][j] + ba[i][j]).abs() < 1e-10);
1106            }
1107        }
1108    }
1109    #[test]
1110    fn test_so3_lie_bracket_cross_product() {
1111        let o1 = [1.0, 0.0, 0.0];
1112        let o2 = [0.0, 1.0, 0.0];
1113        let bracket = so3_lie_bracket(o1, o2);
1114        let cross = cross3(o1, o2);
1115        for i in 0..3 {
1116            assert!((bracket[i] - cross[i]).abs() < 1e-10);
1117        }
1118    }
1119    #[test]
1120    fn test_geodesic_curve_flat_plane() {
1121        let r_fn = |u: f64, v: f64| [u, v, 0.0];
1122        let curve = geodesic_curve(&r_fn, 0.0, 0.0, 1.0, 0.0, 10, 0.1);
1123        assert_eq!(curve.len(), 11);
1124        for (u, v) in &curve {
1125            assert!(
1126                v.abs() < 1e-8,
1127                "v should be ~0 on flat plane geodesic, got {}",
1128                v
1129            );
1130            let _ = u;
1131        }
1132    }
1133    #[test]
1134    fn test_gauss_bonnet_sphere() {
1135        let r = 1.0_f64;
1136        let r_fn = |u: f64, v: f64| {
1137            let x = r * u.sin() * v.cos();
1138            let y = r * u.sin() * v.sin();
1139            let z = r * u.cos();
1140            [x, y, z]
1141        };
1142        let integral = gauss_bonnet_integral(&r_fn, 0.05, PI - 0.05, 0.0, 2.0 * PI, 20, 40);
1143        assert!(
1144            (integral - 4.0 * PI).abs() < 0.5,
1145            "Gauss-Bonnet integral = {}, expected ~4π",
1146            integral
1147        );
1148    }
1149    #[test]
1150    fn test_skew_vee_roundtrip() {
1151        let v = [1.0, 2.0, 3.0];
1152        let m = skew3(v);
1153        let v2 = vee3(m);
1154        for i in 0..3 {
1155            assert!((v[i] - v2[i]).abs() < 1e-10);
1156        }
1157    }
1158    #[test]
1159    fn test_se3_interp_fraction_zero() {
1160        let se3_a = Se3::from_rt(So3::exp([0.1, -0.2, 0.3]), [1.0, 2.0, 3.0]);
1161        let se3_b = Se3::from_rt(So3::exp([0.4, 0.1, -0.5]), [4.0, -1.0, 2.0]);
1162        let interp = se3_a.interp(&se3_b, 0.0);
1163        let t = interp.translation;
1164        let t_a = se3_a.translation;
1165        for i in 0..3 {
1166            assert!((t[i] - t_a[i]).abs() < 1e-8);
1167        }
1168    }
1169    /// Euclidean 2D metric function.
1170    fn flat_2d_metric(_p: &[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM] {
1171        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1172        g[0][0] = 1.0;
1173        g[1][1] = 1.0;
1174        g
1175    }
1176    /// Polar coordinates metric: ds^2 = dr^2 + r^2 d\theta^2.
1177    fn polar_metric(p: &[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM] {
1178        let r = p[0];
1179        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1180        g[0][0] = 1.0;
1181        g[1][1] = r * r;
1182        g
1183    }
1184    /// Sphere metric (2D): ds^2 = R^2 (dtheta^2 + sin^2(theta) dphi^2).
1185    fn sphere_2d_metric(p: &[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM] {
1186        let theta = p[0];
1187        let r = 1.0;
1188        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1189        g[0][0] = r * r;
1190        g[1][1] = r * r * theta.sin() * theta.sin();
1191        g
1192    }
1193    /// Euclidean 3D metric function.
1194    fn flat_3d_metric(_p: &[f64; MAX_DIM]) -> [[f64; MAX_DIM]; MAX_DIM] {
1195        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1196        g[0][0] = 1.0;
1197        g[1][1] = 1.0;
1198        g[2][2] = 1.0;
1199        g
1200    }
1201    #[test]
1202    fn test_metric_tensor_nd_euclidean() {
1203        let m = MetricTensorND::euclidean(3);
1204        assert!((m.determinant() - 1.0).abs() < 1e-12);
1205        let inv = m.inverse();
1206        for i in 0..3 {
1207            for j in 0..3 {
1208                let expected = if i == j { 1.0 } else { 0.0 };
1209                assert!((inv[i][j] - expected).abs() < 1e-12);
1210            }
1211        }
1212    }
1213    #[test]
1214    fn test_metric_tensor_nd_minkowski() {
1215        let m = MetricTensorND::minkowski();
1216        assert!((m.determinant() - (-1.0)).abs() < 1e-12);
1217        let inv = m.inverse();
1218        assert!((inv[0][0] - (-1.0)).abs() < 1e-12);
1219        assert!((inv[1][1] - 1.0).abs() < 1e-12);
1220    }
1221    #[test]
1222    fn test_metric_tensor_nd_inverse_roundtrip() {
1223        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1224        g[0][0] = 2.0;
1225        g[0][1] = 0.5;
1226        g[1][0] = 0.5;
1227        g[1][1] = 3.0;
1228        let m = MetricTensorND::new(2, &g);
1229        let inv = m.inverse();
1230        for i in 0..2 {
1231            for j in 0..2 {
1232                let mut val = 0.0;
1233                for k in 0..2 {
1234                    val += g[i][k] * inv[k][j];
1235                }
1236                let expected = if i == j { 1.0 } else { 0.0 };
1237                assert!(
1238                    (val - expected).abs() < 1e-10,
1239                    "g * g^-1 [{i}][{j}] = {val}, expected {expected}"
1240                );
1241            }
1242        }
1243    }
1244    #[test]
1245    fn test_metric_raise_lower_roundtrip() {
1246        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1247        g[0][0] = 2.0;
1248        g[0][1] = 0.5;
1249        g[1][0] = 0.5;
1250        g[1][1] = 3.0;
1251        let m = MetricTensorND::new(2, &g);
1252        let mut v = [0.0; MAX_DIM];
1253        v[0] = 1.0;
1254        v[1] = 2.0;
1255        let lowered = m.lower_index(&v);
1256        let raised = m.raise_index(&lowered);
1257        for i in 0..2 {
1258            assert!(
1259                (raised[i] - v[i]).abs() < 1e-10,
1260                "Raise/lower roundtrip failed at {i}"
1261            );
1262        }
1263    }
1264    #[test]
1265    fn test_metric_inner_product_euclidean() {
1266        let m = MetricTensorND::euclidean(3);
1267        let mut u = [0.0; MAX_DIM];
1268        let mut v = [0.0; MAX_DIM];
1269        u[0] = 1.0;
1270        u[1] = 2.0;
1271        u[2] = 3.0;
1272        v[0] = 4.0;
1273        v[1] = 5.0;
1274        v[2] = 6.0;
1275        let ip = m.inner_product(&u, &v);
1276        assert!((ip - 32.0).abs() < 1e-12);
1277    }
1278    #[test]
1279    fn test_metric_volume_element() {
1280        let mut g = [[0.0; MAX_DIM]; MAX_DIM];
1281        g[0][0] = 4.0;
1282        g[1][1] = 9.0;
1283        let m = MetricTensorND::new(2, &g);
1284        assert!((m.volume_element() - 6.0).abs() < 1e-12);
1285    }
1286    #[test]
1287    fn test_levi_civita_flat_space() {
1288        let point = [0.5, 0.5, 0.0, 0.0];
1289        let conn = LeviCivitaConnection::from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1290        for sigma in 0..2 {
1291            for mu in 0..2 {
1292                for nu in 0..2 {
1293                    assert!(
1294                        conn.christoffel[sigma][mu][nu].abs() < 1e-6,
1295                        "Christoffel[{sigma}][{mu}][{nu}] = {} in flat space",
1296                        conn.christoffel[sigma][mu][nu]
1297                    );
1298                }
1299            }
1300        }
1301    }
1302    #[test]
1303    fn test_levi_civita_polar_coords() {
1304        let r = 2.0;
1305        let point = [r, 1.0, 0.0, 0.0];
1306        let conn = LeviCivitaConnection::from_metric_fn(&polar_metric, &point, 2, 1e-5);
1307        assert!(
1308            (conn.christoffel[0][1][1] - (-r)).abs() < 0.01,
1309            "Gamma^r_{{theta theta}} = {}, expected {}",
1310            conn.christoffel[0][1][1],
1311            -r
1312        );
1313        assert!(
1314            (conn.christoffel[1][0][1] - 1.0 / r).abs() < 0.01,
1315            "Gamma^theta_{{r theta}} = {}, expected {}",
1316            conn.christoffel[1][0][1],
1317            1.0 / r
1318        );
1319    }
1320    #[test]
1321    fn test_riemann_tensor_flat_space() {
1322        let point = [0.5, 0.5, 0.0, 0.0];
1323        let riemann = RiemannTensor::from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1324        for rho in 0..2 {
1325            for sigma in 0..2 {
1326                for mu in 0..2 {
1327                    for nu in 0..2 {
1328                        assert!(
1329                            riemann.components[rho][sigma][mu][nu].abs() < 1e-3,
1330                            "R[{rho}][{sigma}][{mu}][{nu}] = {} in flat space",
1331                            riemann.components[rho][sigma][mu][nu]
1332                        );
1333                    }
1334                }
1335            }
1336        }
1337    }
1338    #[test]
1339    fn test_riemann_tensor_sphere() {
1340        let theta = 1.0;
1341        let point = [theta, 1.0, 0.0, 0.0];
1342        let riemann = RiemannTensor::from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1343        let mut max_component = 0.0_f64;
1344        for rho in 0..2 {
1345            for sigma in 0..2 {
1346                for mu in 0..2 {
1347                    for nu in 0..2 {
1348                        max_component =
1349                            max_component.max(riemann.components[rho][sigma][mu][nu].abs());
1350                    }
1351                }
1352            }
1353        }
1354        assert!(
1355            max_component > 0.1,
1356            "Sphere should have non-zero Riemann tensor"
1357        );
1358    }
1359    #[test]
1360    fn test_riemann_bianchi_identity() {
1361        let theta = 1.0;
1362        let point = [theta, 1.0, 0.0, 0.0];
1363        let riemann = RiemannTensor::from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1364        let viol = riemann.bianchi_identity_violation();
1365        assert!(
1366            viol < 0.5,
1367            "Bianchi identity violation = {viol}, should be small"
1368        );
1369    }
1370    #[test]
1371    fn test_ricci_tensor_flat_space() {
1372        let point = [0.5, 0.5, 0.0, 0.0];
1373        let ricci = RicciTensor::from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1374        for mu in 0..2 {
1375            for nu in 0..2 {
1376                assert!(
1377                    ricci.components[mu][nu].abs() < 1e-3,
1378                    "Ricci[{mu}][{nu}] = {} in flat space",
1379                    ricci.components[mu][nu]
1380                );
1381            }
1382        }
1383    }
1384    #[test]
1385    fn test_ricci_tensor_symmetry() {
1386        let theta = 1.0;
1387        let point = [theta, 1.0, 0.0, 0.0];
1388        let ricci = RicciTensor::from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1389        let viol = ricci.symmetry_violation();
1390        assert!(
1391            viol < 0.1,
1392            "Ricci tensor should be symmetric, violation = {viol}"
1393        );
1394    }
1395    #[test]
1396    fn test_ricci_scalar_flat_space() {
1397        let point = [0.5, 0.5, 0.0, 0.0];
1398        let r = ricci_scalar_from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1399        assert!(r.abs() < 0.01, "Ricci scalar in flat 2D = {r}, expected 0");
1400    }
1401    #[test]
1402    fn test_ricci_scalar_sphere() {
1403        let theta = 1.0;
1404        let point = [theta, 1.0, 0.0, 0.0];
1405        let r = ricci_scalar_from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1406        assert!(
1407            (r - 2.0).abs() < 0.5,
1408            "Ricci scalar on unit sphere = {r}, expected ~2"
1409        );
1410    }
1411    #[test]
1412    fn test_einstein_tensor_flat_space() {
1413        let point = [0.5, 0.5, 0.0, 0.0];
1414        let ricci = RicciTensor::from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1415        let g_here = flat_2d_metric(&point);
1416        let metric = MetricTensorND { dim: 2, g: g_here };
1417        let g_tensor = einstein_tensor(&metric, &ricci);
1418        for mu in 0..2 {
1419            for nu in 0..2 {
1420                assert!(
1421                    g_tensor[mu][nu].abs() < 0.01,
1422                    "Einstein[{mu}][{nu}] = {} in flat space",
1423                    g_tensor[mu][nu]
1424                );
1425            }
1426        }
1427    }
1428    #[test]
1429    fn test_covariant_derivative_flat_space() {
1430        let point = [0.5, 0.5, 0.0, 0.0];
1431        let conn = LeviCivitaConnection::from_metric_fn(&flat_2d_metric, &point, 2, 1e-4);
1432        let mut v = [0.0; MAX_DIM];
1433        v[0] = 1.0;
1434        v[1] = 2.0;
1435        let mut dv = [[0.0; MAX_DIM]; MAX_DIM];
1436        dv[0][0] = 0.5;
1437        dv[1][1] = -0.3;
1438        let nabla = covariant_derivative_vector(&conn, &v, &dv);
1439        assert!((nabla[0][0] - 0.5).abs() < 0.01);
1440        assert!((nabla[1][1] - (-0.3)).abs() < 0.01);
1441    }
1442    #[test]
1443    fn test_geodesic_equation_flat_space() {
1444        let mut x0 = [0.0; MAX_DIM];
1445        let mut v0 = [0.0; MAX_DIM];
1446        x0[0] = 0.0;
1447        x0[1] = 0.0;
1448        v0[0] = 1.0;
1449        v0[1] = 0.5;
1450        let traj = geodesic_equation_solve(&flat_2d_metric, 2, &x0, &v0, 0.1, 10, 1e-4);
1451        assert_eq!(traj.len(), 11);
1452        let (final_x, _) = &traj[10];
1453        assert!(
1454            (final_x[0] - 1.0).abs() < 0.05,
1455            "x = {}, expected ~1.0",
1456            final_x[0]
1457        );
1458        assert!(
1459            (final_x[1] - 0.5).abs() < 0.05,
1460            "y = {}, expected ~0.5",
1461            final_x[1]
1462        );
1463    }
1464    #[test]
1465    fn test_parallel_transport_nd_flat() {
1466        let curve: Vec<[f64; MAX_DIM]> = (0..10)
1467            .map(|i| {
1468                let mut p = [0.0; MAX_DIM];
1469                p[0] = i as f64 * 0.1;
1470                p[1] = i as f64 * 0.05;
1471                p
1472            })
1473            .collect();
1474        let mut v0 = [0.0; MAX_DIM];
1475        v0[0] = 1.0;
1476        v0[1] = 0.0;
1477        let transported = parallel_transport_nd(&flat_2d_metric, 2, &curve, &v0, 1e-4);
1478        let last = transported.last().unwrap();
1479        assert!((last[0] - 1.0).abs() < 0.01);
1480        assert!(last[1].abs() < 0.01);
1481    }
1482    #[test]
1483    fn test_killing_vector_rotation() {
1484        let xi_fn = |p: &[f64; MAX_DIM]| -> [f64; MAX_DIM] {
1485            let mut xi = [0.0; MAX_DIM];
1486            xi[0] = -p[1];
1487            xi[1] = p[0];
1488            xi
1489        };
1490        let point = [1.0, 1.0, 0.0, 0.0];
1491        let viol = killing_equation_violation(&flat_2d_metric, &xi_fn, &point, 2, 1e-4);
1492        assert!(
1493            viol < 0.01,
1494            "Rotation Killing vector violation = {viol}, should be ~0"
1495        );
1496    }
1497    #[test]
1498    fn test_killing_vector_translation() {
1499        let xi_fn = |_p: &[f64; MAX_DIM]| -> [f64; MAX_DIM] {
1500            let mut xi = [0.0; MAX_DIM];
1501            xi[0] = 1.0;
1502            xi
1503        };
1504        let point = [2.0, 3.0, 0.0, 0.0];
1505        let viol = killing_equation_violation(&flat_2d_metric, &xi_fn, &point, 2, 1e-4);
1506        assert!(
1507            viol < 0.01,
1508            "Translation Killing vector violation = {viol}, should be ~0"
1509        );
1510    }
1511    #[test]
1512    fn test_non_killing_vector() {
1513        let xi_fn = |p: &[f64; MAX_DIM]| -> [f64; MAX_DIM] {
1514            let mut xi = [0.0; MAX_DIM];
1515            xi[0] = p[0];
1516            xi[1] = p[1];
1517            xi
1518        };
1519        let point = [1.0, 1.0, 0.0, 0.0];
1520        let viol = killing_equation_violation(&flat_2d_metric, &xi_fn, &point, 2, 1e-4);
1521        assert!(
1522            viol > 0.5,
1523            "Dilation should NOT be a Killing vector, violation = {viol}"
1524        );
1525    }
1526    #[test]
1527    fn test_geodesic_deviation_flat_space() {
1528        let x0 = [0.0; MAX_DIM];
1529        let mut v0 = [0.0; MAX_DIM];
1530        v0[0] = 1.0;
1531        let traj = geodesic_equation_solve(&flat_2d_metric, 2, &x0, &v0, 0.1, 10, 1e-4);
1532        let mut xi0 = [0.0; MAX_DIM];
1533        xi0[1] = 1.0;
1534        let dxi0 = [0.0; MAX_DIM];
1535        let deviation = geodesic_deviation(&flat_2d_metric, 2, &traj, &xi0, &dxi0, 0.1, 1e-4);
1536        let last = deviation.last().unwrap();
1537        assert!(
1538            (last[1] - 1.0).abs() < 0.05,
1539            "Deviation should remain ~1.0, got {}",
1540            last[1]
1541        );
1542    }
1543    #[test]
1544    fn test_gauss_bonnet_2d_flat() {
1545        let point = [1.0, 1.0, 0.0, 0.0];
1546        let integrand = gauss_bonnet_2d_integrand(&flat_2d_metric, &point, 1e-4);
1547        assert!(
1548            integrand.abs() < 0.01,
1549            "Flat space GB integrand = {integrand}"
1550        );
1551    }
1552    #[test]
1553    fn test_levi_civita_connection_symmetry() {
1554        let theta = 1.0;
1555        let point = [theta, 1.0, 0.0, 0.0];
1556        let conn = LeviCivitaConnection::from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1557        for sigma in 0..2 {
1558            for mu in 0..2 {
1559                for nu in 0..2 {
1560                    let diff =
1561                        (conn.christoffel[sigma][mu][nu] - conn.christoffel[sigma][nu][mu]).abs();
1562                    assert!(
1563                        diff < 0.01,
1564                        "Christoffel not symmetric: [{sigma}][{mu}][{nu}] diff = {diff}"
1565                    );
1566                }
1567            }
1568        }
1569    }
1570    #[test]
1571    fn test_weyl_tensor_3d_flat() {
1572        let point = [1.0, 1.0, 1.0, 0.0];
1573        let weyl = weyl_tensor(&flat_3d_metric, &point, 3, 1e-4);
1574        for rho in 0..3 {
1575            for sigma in 0..3 {
1576                for mu in 0..3 {
1577                    for nu in 0..3 {
1578                        assert!(
1579                            weyl[rho][sigma][mu][nu].abs() < 0.1,
1580                            "Weyl[{rho}][{sigma}][{mu}][{nu}] = {} in flat 3D",
1581                            weyl[rho][sigma][mu][nu]
1582                        );
1583                    }
1584                }
1585            }
1586        }
1587    }
1588    #[test]
1589    fn test_riemann_antisymmetry() {
1590        let theta = 1.0;
1591        let point = [theta, 1.0, 0.0, 0.0];
1592        let riemann = RiemannTensor::from_metric_fn(&sphere_2d_metric, &point, 2, 1e-4);
1593        for rho in 0..2 {
1594            for sigma in 0..2 {
1595                for mu in 0..2 {
1596                    for nu in 0..2 {
1597                        let sum = riemann.components[rho][sigma][mu][nu]
1598                            + riemann.components[rho][sigma][nu][mu];
1599                        assert!(
1600                            sum.abs() < 0.1,
1601                            "Riemann not antisymmetric in last pair: [{rho}][{sigma}][{mu}][{nu}], sum = {sum}"
1602                        );
1603                    }
1604                }
1605            }
1606        }
1607    }
1608    #[test]
1609    fn test_metric_determinant_4d() {
1610        let m = MetricTensorND::minkowski();
1611        assert!((m.determinant() - (-1.0)).abs() < 1e-12);
1612    }
1613}