use crate::HisabError;
#[allow(clippy::needless_range_loop)]
pub fn christoffel_symbols(
dim: usize,
g_inv: &dyn Fn(usize, usize) -> f64,
dg: &dyn Fn(usize, usize, usize) -> f64,
) -> Result<Vec<f64>, HisabError> {
if dim == 0 {
return Err(HisabError::InvalidInput(
"dimension must be positive".into(),
));
}
tracing::debug!(dim, "computing Christoffel symbols");
let n3 = dim * dim * dim;
let mut gamma = vec![0.0; n3];
for alpha in 0..dim {
for mu in 0..dim {
for nu in 0..dim {
let mut sum = 0.0;
for lambda in 0..dim {
let g_al = g_inv(alpha, lambda);
let bracket = dg(nu, lambda, mu) + dg(mu, lambda, nu) - dg(mu, nu, lambda);
sum += g_al * bracket;
}
gamma[alpha * dim * dim + mu * dim + nu] = 0.5 * sum;
}
}
}
Ok(gamma)
}
#[must_use]
#[inline]
pub fn christoffel_get(gamma: &[f64], dim: usize, alpha: usize, mu: usize, nu: usize) -> f64 {
gamma[alpha * dim * dim + mu * dim + nu]
}
#[allow(clippy::needless_range_loop)]
pub fn riemann_tensor(
dim: usize,
gamma: &[f64],
dgamma: &dyn Fn(usize, usize, usize, usize) -> f64,
) -> Result<Vec<f64>, HisabError> {
if dim == 0 {
return Err(HisabError::InvalidInput(
"dimension must be positive".into(),
));
}
tracing::debug!(dim, "computing Riemann tensor");
let d4 = dim * dim * dim * dim;
let mut riemann = vec![0.0; d4];
let d2 = dim * dim;
for rho in 0..dim {
for sigma in 0..dim {
for mu in 0..dim {
for nu in 0..dim {
let mut val = dgamma(rho, nu, sigma, mu) - dgamma(rho, mu, sigma, nu);
for lambda in 0..dim {
val += gamma[rho * d2 + mu * dim + lambda]
* gamma[lambda * d2 + nu * dim + sigma]
- gamma[rho * d2 + nu * dim + lambda]
* gamma[lambda * d2 + mu * dim + sigma];
}
riemann[rho * dim * d2 + sigma * d2 + mu * dim + nu] = val;
}
}
}
}
Ok(riemann)
}
#[must_use]
#[inline]
pub fn riemann_get(
riemann: &[f64],
dim: usize,
rho: usize,
sigma: usize,
mu: usize,
nu: usize,
) -> f64 {
riemann[rho * dim * dim * dim + sigma * dim * dim + mu * dim + nu]
}
#[allow(clippy::needless_range_loop)]
#[must_use]
pub fn ricci_tensor(riemann: &[f64], dim: usize) -> Vec<f64> {
let mut ricci = vec![0.0; dim * dim];
let d3 = dim * dim * dim;
let d2 = dim * dim;
for mu in 0..dim {
for nu in 0..dim {
let mut sum = 0.0;
for lambda in 0..dim {
sum += riemann[lambda * d3 + mu * d2 + lambda * dim + nu];
}
ricci[mu * dim + nu] = sum;
}
}
ricci
}
#[allow(clippy::needless_range_loop)]
#[must_use]
pub fn ricci_scalar(ricci: &[f64], dim: usize, g_inv: &dyn Fn(usize, usize) -> f64) -> f64 {
let mut scalar = 0.0;
for mu in 0..dim {
for nu in 0..dim {
scalar += g_inv(mu, nu) * ricci[mu * dim + nu];
}
}
scalar
}
#[allow(clippy::needless_range_loop)]
#[must_use]
pub fn einstein_tensor(
ricci: &[f64],
ricci_scalar_val: f64,
dim: usize,
g: &dyn Fn(usize, usize) -> f64,
) -> Vec<f64> {
let mut einstein = vec![0.0; dim * dim];
for mu in 0..dim {
for nu in 0..dim {
einstein[mu * dim + nu] = ricci[mu * dim + nu] - 0.5 * ricci_scalar_val * g(mu, nu);
}
}
einstein
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GeodesicState {
pub x: Vec<f64>,
pub u: Vec<f64>,
}
impl std::fmt::Display for GeodesicState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "GeodesicState(dim={}, x=[", self.x.len())?;
for (i, v) in self.x.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v:.3}")?;
}
write!(f, "], u=[")?;
for (i, v) in self.u.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v:.3}")?;
}
write!(f, "])")
}
}
pub fn geodesic_rk4(
dim: usize,
gamma_fn: &dyn Fn(&[f64]) -> Vec<f64>,
initial: &GeodesicState,
dtau: f64,
steps: usize,
) -> Result<Vec<GeodesicState>, HisabError> {
if initial.x.len() != dim || initial.u.len() != dim {
return Err(HisabError::InvalidInput(
"initial state dimensions don't match".into(),
));
}
tracing::debug!(dim, steps, "geodesic RK4 integration");
let mut trajectory = Vec::with_capacity(steps + 1);
trajectory.push(initial.clone());
let mut x = initial.x.clone();
let mut u = initial.u.clone();
for _ in 0..steps {
let accel = |pos: &[f64], vel: &[f64]| -> Vec<f64> {
let gamma = gamma_fn(pos);
let mut acc = vec![0.0; dim];
for (alpha, acc_a) in acc.iter_mut().enumerate() {
let mut sum = 0.0;
for mu in 0..dim {
for nu in 0..dim {
sum += christoffel_get(&gamma, dim, alpha, mu, nu) * vel[mu] * vel[nu];
}
}
*acc_a = -sum;
}
acc
};
let k1x: Vec<f64> = u.iter().map(|&v| dtau * v).collect();
let a1 = accel(&x, &u);
let k1u: Vec<f64> = a1.iter().map(|&a| dtau * a).collect();
let x2: Vec<f64> = x
.iter()
.zip(k1x.iter())
.map(|(&xi, &ki)| xi + 0.5 * ki)
.collect();
let u2: Vec<f64> = u
.iter()
.zip(k1u.iter())
.map(|(&ui, &ki)| ui + 0.5 * ki)
.collect();
let k2x: Vec<f64> = u2.iter().map(|&v| dtau * v).collect();
let a2 = accel(&x2, &u2);
let k2u: Vec<f64> = a2.iter().map(|&a| dtau * a).collect();
let x3: Vec<f64> = x
.iter()
.zip(k2x.iter())
.map(|(&xi, &ki)| xi + 0.5 * ki)
.collect();
let u3: Vec<f64> = u
.iter()
.zip(k2u.iter())
.map(|(&ui, &ki)| ui + 0.5 * ki)
.collect();
let k3x: Vec<f64> = u3.iter().map(|&v| dtau * v).collect();
let a3 = accel(&x3, &u3);
let k3u: Vec<f64> = a3.iter().map(|&a| dtau * a).collect();
let x4: Vec<f64> = x.iter().zip(k3x.iter()).map(|(&xi, &ki)| xi + ki).collect();
let u4: Vec<f64> = u.iter().zip(k3u.iter()).map(|(&ui, &ki)| ui + ki).collect();
let k4x: Vec<f64> = u4.iter().map(|&v| dtau * v).collect();
let a4 = accel(&x4, &u4);
let k4u: Vec<f64> = a4.iter().map(|&a| dtau * a).collect();
for i in 0..dim {
x[i] += (k1x[i] + 2.0 * k2x[i] + 2.0 * k3x[i] + k4x[i]) / 6.0;
u[i] += (k1u[i] + 2.0 * k2u[i] + 2.0 * k3u[i] + k4u[i]) / 6.0;
}
trajectory.push(GeodesicState {
x: x.clone(),
u: u.clone(),
});
}
Ok(trajectory)
}
#[allow(clippy::needless_range_loop)]
#[must_use]
pub fn killing_residual(
dim: usize,
xi: &[f64],
dxi: &dyn Fn(usize, usize) -> f64,
gamma: &[f64],
g: &dyn Fn(usize, usize) -> f64,
) -> f64 {
let mut max_residual = 0.0_f64;
for mu in 0..dim {
for nu in mu..dim {
let mut nabla_mu_xi_nu = 0.0;
for lambda in 0..dim {
nabla_mu_xi_nu += g(nu, lambda) * dxi(lambda, mu);
for rho in 0..dim {
nabla_mu_xi_nu +=
g(nu, lambda) * christoffel_get(gamma, dim, lambda, mu, rho) * xi[rho];
}
}
let mut nabla_nu_xi_mu = 0.0;
for lambda in 0..dim {
nabla_nu_xi_mu += g(mu, lambda) * dxi(lambda, nu);
for rho in 0..dim {
nabla_nu_xi_mu +=
g(mu, lambda) * christoffel_get(gamma, dim, lambda, nu, rho) * xi[rho];
}
}
let residual = (nabla_mu_xi_nu + nabla_nu_xi_mu).abs();
max_residual = max_residual.max(residual);
}
}
max_residual
}
pub fn wedge_1_1(alpha: &[f64], beta: &[f64], dim: usize) -> Result<Vec<f64>, HisabError> {
if alpha.len() != dim || beta.len() != dim {
return Err(HisabError::InvalidInput(format!(
"1-form dimension mismatch: expected {dim}"
)));
}
let n2 = dim * (dim - 1) / 2;
let mut result = Vec::with_capacity(n2);
for i in 0..dim {
for j in (i + 1)..dim {
result.push(alpha[i] * beta[j] - alpha[j] * beta[i]);
}
}
Ok(result)
}
pub fn wedge_1_2(alpha: &[f64], beta: &[f64], dim: usize) -> Result<Vec<f64>, HisabError> {
if alpha.len() != dim {
return Err(HisabError::InvalidInput("1-form dimension mismatch".into()));
}
let n2 = dim * (dim - 1) / 2;
if beta.len() != n2 {
return Err(HisabError::InvalidInput("2-form dimension mismatch".into()));
}
let n3 = dim * (dim - 1) * (dim - 2) / 6;
let mut result = vec![0.0; n3];
let beta_idx = |i: usize, j: usize| -> f64 {
if i < j {
let pos = i * dim - i * (i + 1) / 2 + (j - i - 1);
beta[pos]
} else if j < i {
let pos = j * dim - j * (j + 1) / 2 + (i - j - 1);
-beta[pos]
} else {
0.0
}
};
let mut pos = 0;
for i in 0..dim {
for j in (i + 1)..dim {
for k in (j + 1)..dim {
result[pos] = alpha[i] * beta_idx(j, k) - alpha[j] * beta_idx(i, k)
+ alpha[k] * beta_idx(i, j);
pos += 1;
}
}
}
Ok(result)
}
pub fn hodge_star_2form_4d(form: &[f64], metric_det_sign: f64) -> Result<[f64; 6], HisabError> {
if form.len() != 6 {
return Err(HisabError::InvalidInput(
"expected 6-component 2-form in 4D".into(),
));
}
let _ = metric_det_sign;
Ok([
form[5], -form[4], form[3], -form[2], form[1], -form[0], ])
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-8;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < TOL
}
#[test]
fn flat_spacetime_christoffels() {
let g_inv = |mu: usize, nu: usize| -> f64 {
if mu == nu {
if mu == 0 { 1.0 } else { -1.0 }
} else {
0.0
}
};
let dg = |_i: usize, _j: usize, _k: usize| -> f64 { 0.0 };
let gamma = christoffel_symbols(4, &g_inv, &dg).unwrap();
for &g in &gamma {
assert!(approx(g, 0.0), "flat spacetime Christoffel should be 0");
}
}
#[test]
fn flat_spacetime_riemann() {
let gamma = vec![0.0; 64]; let dgamma = |_rho: usize, _mu: usize, _nu: usize, _k: usize| -> f64 { 0.0 };
let riemann = riemann_tensor(4, &gamma, &dgamma).unwrap();
for &r in &riemann {
assert!(approx(r, 0.0), "flat spacetime Riemann should be 0");
}
}
#[test]
fn flat_spacetime_ricci() {
let riemann = vec![0.0; 256];
let ricci = ricci_tensor(&riemann, 4);
for &r in &ricci {
assert!(approx(r, 0.0));
}
}
#[test]
fn flat_spacetime_ricci_scalar() {
let ricci = vec![0.0; 16];
let g_inv = |mu: usize, nu: usize| -> f64 {
if mu == nu {
if mu == 0 { 1.0 } else { -1.0 }
} else {
0.0
}
};
assert!(approx(ricci_scalar(&ricci, 4, &g_inv), 0.0));
}
#[test]
fn sphere_christoffel_symbols() {
let theta = std::f64::consts::FRAC_PI_4;
let sin_t = theta.sin();
let cos_t = theta.cos();
let g_inv = |i: usize, j: usize| -> f64 {
if i == j {
if i == 0 { 1.0 } else { 1.0 / (sin_t * sin_t) }
} else {
0.0
}
};
let dg = |i: usize, j: usize, k: usize| -> f64 {
if i == 1 && j == 1 && k == 0 {
2.0 * sin_t * cos_t
} else {
0.0
}
};
let gamma = christoffel_symbols(2, &g_inv, &dg).unwrap();
assert!(
approx(christoffel_get(&gamma, 2, 0, 1, 1), -sin_t * cos_t),
"Γ^θ_φφ failed"
);
let cot = cos_t / sin_t;
assert!(
approx(christoffel_get(&gamma, 2, 1, 0, 1), cot),
"Γ^φ_θφ failed"
);
assert!(
approx(christoffel_get(&gamma, 2, 1, 1, 0), cot),
"Γ^φ_φθ failed"
);
}
#[test]
fn geodesic_flat_space_straight_line() {
let gamma_fn = |_pos: &[f64]| -> Vec<f64> {
vec![0.0; 27] };
let initial = GeodesicState {
x: vec![0.0, 0.0, 0.0],
u: vec![1.0, 2.0, 3.0],
};
let traj = geodesic_rk4(3, &gamma_fn, &initial, 0.01, 100).unwrap();
let final_state = &traj[100];
assert!(approx(final_state.x[0], 1.0));
assert!(approx(final_state.x[1], 2.0));
assert!(approx(final_state.x[2], 3.0));
}
#[test]
fn flat_space_translation_is_killing() {
let xi = [1.0, 0.0, 0.0];
let dxi = |_mu: usize, _nu: usize| -> f64 { 0.0 }; let gamma = vec![0.0; 27]; let g = |i: usize, j: usize| -> f64 { if i == j { 1.0 } else { 0.0 } };
let residual = killing_residual(3, &xi, &dxi, &gamma, &g);
assert!(residual < 1e-10);
}
#[test]
fn wedge_1_1_antisymmetric() {
let alpha = vec![1.0, 0.0, 0.0];
let beta = vec![0.0, 1.0, 0.0];
let result = wedge_1_1(&alpha, &beta, 3).unwrap();
assert!(approx(result[0], 1.0));
assert!(approx(result[1], 0.0));
assert!(approx(result[2], 0.0));
}
#[test]
fn wedge_self_vanishes() {
let alpha = vec![1.0, 2.0, 3.0];
let result = wedge_1_1(&alpha, &alpha, 3).unwrap();
for &v in &result {
assert!(approx(v, 0.0), "α ∧ α should vanish");
}
}
#[test]
fn hodge_star_involution() {
let f = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let star_f = hodge_star_2form_4d(&f, -1.0).unwrap();
let star_star_f = hodge_star_2form_4d(&star_f, -1.0).unwrap();
for i in 0..6 {
assert!(approx(star_star_f[i], -f[i]), "**F ≠ -F at component {i}");
}
}
#[test]
fn christoffel_dim_zero() {
assert!(christoffel_symbols(0, &|_, _| 0.0, &|_, _, _| 0.0).is_err());
}
#[test]
fn riemann_dim_zero() {
assert!(riemann_tensor(0, &[], &|_, _, _, _| 0.0).is_err());
}
#[test]
fn geodesic_dim_mismatch() {
let state = GeodesicState {
x: vec![0.0, 0.0],
u: vec![1.0, 0.0],
};
assert!(geodesic_rk4(3, &|_| vec![0.0; 27], &state, 0.01, 10).is_err());
}
#[test]
fn wedge_1_1_dim_mismatch() {
assert!(wedge_1_1(&[1.0, 0.0], &[1.0, 0.0, 0.0], 3).is_err());
}
#[test]
fn sphere_christoffel_symmetry() {
let theta = std::f64::consts::FRAC_PI_4;
let sin_t = theta.sin();
let cos_t = theta.cos();
let g_inv = |i: usize, j: usize| -> f64 {
if i == j {
if i == 0 { 1.0 } else { 1.0 / (sin_t * sin_t) }
} else {
0.0
}
};
let dg = |i: usize, j: usize, k: usize| -> f64 {
if i == 1 && j == 1 && k == 0 {
2.0 * sin_t * cos_t
} else {
0.0
}
};
let gamma = christoffel_symbols(2, &g_inv, &dg).unwrap();
for alpha in 0..2 {
for mu in 0..2 {
for nu in 0..2 {
assert!(approx(
christoffel_get(&gamma, 2, alpha, mu, nu),
christoffel_get(&gamma, 2, alpha, nu, mu),
));
}
}
}
}
#[test]
fn einstein_flat() {
let ricci = vec![0.0; 16];
let g = |i: usize, j: usize| -> f64 {
if i == j {
if i == 0 { 1.0 } else { -1.0 }
} else {
0.0
}
};
let einstein = einstein_tensor(&ricci, 0.0, 4, &g);
for &e in &einstein {
assert!(approx(e, 0.0));
}
}
}