1#![allow(clippy::needless_range_loop)]
6use super::types::{
7 GeodesicState, LeviCivitaConnection, MetricTensorND, OneForm, RicciTensor, RiemannTensor,
8 RiemannianMetric, SecondFundamentalForm, So3, TwoForm,
9};
10
11pub type Vec3 = [f64; 3];
13pub type Mat3 = [[f64; 3]; 3];
15pub type Mat4 = [[f64; 4]; 4];
17pub type Quat = [f64; 4];
19pub fn add3(a: Vec3, b: Vec3) -> Vec3 {
21 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
22}
23pub fn sub3(a: Vec3, b: Vec3) -> Vec3 {
25 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27pub fn scale3(a: Vec3, s: f64) -> Vec3 {
29 [a[0] * s, a[1] * s, a[2] * s]
30}
31pub fn dot3(a: Vec3, b: Vec3) -> f64 {
33 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
34}
35pub 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}
43pub fn norm3(a: Vec3) -> f64 {
45 dot3(a, a).sqrt()
46}
47pub 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}
56pub fn mat3_identity() -> Mat3 {
58 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
59}
60pub fn mat3_zero() -> Mat3 {
62 [[0.0; 3]; 3]
63}
64pub fn mat3_mul_vec3(m: Mat3, v: Vec3) -> Vec3 {
66 [dot3(m[0], v), dot3(m[1], v), dot3(m[2], v)]
67}
68pub 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}
77pub 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}
85pub fn mat3_trace(m: Mat3) -> f64 {
87 m[0][0] + m[1][1] + m[2][2]
88}
89pub 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}
99pub 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}
109pub 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}
119pub 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}
125pub 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}
129pub fn vee3(m: Mat3) -> Vec3 {
131 [m[2][1], m[0][2], m[1][0]]
132}
133pub 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}
143pub 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}
155pub 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}
163pub fn so3_exp_map(r0: &So3, tangent: Vec3) -> So3 {
170 let step = So3::exp(tangent);
171 r0.compose(&step)
172}
173pub fn so3_log_map(r0: &So3, r1: &So3) -> Vec3 {
177 let rel = r0.inverse().compose(r1);
178 rel.log()
179}
180pub fn so3_geodesic(r0: &So3, r1: &So3, t: f64) -> So3 {
182 r0.slerp(r1, t)
183}
184pub 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}
239pub 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}
260pub 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}
286pub 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}
342pub 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}
386pub 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}
401pub 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}
407pub fn so3_lie_bracket(omega1: Vec3, omega2: Vec3) -> Vec3 {
411 cross3(omega1, omega2)
412}
413pub fn so3_adjoint(r: &So3, omega: Vec3) -> Vec3 {
415 r.rotate(omega)
416}
417pub fn so3_coadjoint(r: &So3, mu: Vec3) -> Vec3 {
419 r.rotate(mu)
420}
421pub fn so3_small_adjoint(omega1: Vec3, omega2: Vec3) -> Vec3 {
423 cross3(omega1, omega2)
424}
425pub const MAX_DIM: usize = 4;
427pub 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}
439pub 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}
454pub 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}
466pub 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}
515pub 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}
537pub 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#[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}
628pub 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#[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}
727pub 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#[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}
828pub 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 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 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 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 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}