use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::{array, Array1, Array2, ArrayView1, ArrayView2};
use std::f64::consts::{PI, TAU};
#[allow(dead_code)]
pub fn cart_to_spherical(cart: &ArrayView1<f64>) -> SpatialResult<Array1<f64>> {
if cart.len() != 3 {
return Err(SpatialError::DimensionError(format!(
"Cartesian coordinates must have 3 elements, got {}",
cart.len()
)));
}
let x = cart[0];
let y = cart[1];
let z = cart[2];
let r = (x * x + y * y + z * z).sqrt();
if r < 1e-10 {
return Ok(Array1::zeros(3));
}
let theta = if r < 1e-10 {
0.0 } else {
(z / r).acos()
};
let phi = if x.abs() < 1e-10 && y.abs() < 1e-10 {
0.0 } else {
let raw_phi = y.atan2(x);
if raw_phi < 0.0 {
raw_phi + TAU
} else {
raw_phi
}
};
Ok(array![r, theta, phi])
}
#[allow(dead_code)]
pub fn spherical_to_cart(spherical: &ArrayView1<f64>) -> SpatialResult<Array1<f64>> {
if spherical.len() != 3 {
return Err(SpatialError::DimensionError(format!(
"Spherical coordinates must have 3 elements, got {}",
spherical.len()
)));
}
let r = spherical[0];
let theta = spherical[1];
let phi = spherical[2];
if r < 0.0 {
return Err(SpatialError::ValueError(format!(
"Radius r must be non-negative, got {r}"
)));
}
if !(0.0..=PI).contains(&theta) {
return Err(SpatialError::ValueError(format!(
"Polar angle theta must be in [0, π], got {theta}"
)));
}
let x = r * theta.sin() * phi.cos();
let y = r * theta.sin() * phi.sin();
let z = r * theta.cos();
Ok(array![x, y, z])
}
#[allow(dead_code)]
pub fn cart_to_spherical_batch(cart: &ArrayView2<'_, f64>) -> SpatialResult<Array2<f64>> {
if cart.ncols() != 3 {
return Err(SpatialError::DimensionError(format!(
"Cartesian coordinates must have 3 columns, got {}",
cart.ncols()
)));
}
let n_points = cart.nrows();
let mut result = Array2::zeros((n_points, 3));
for (i, row) in cart.rows().into_iter().enumerate() {
let spherical = cart_to_spherical(&row)?;
result.row_mut(i).assign(&spherical);
}
Ok(result)
}
#[allow(dead_code)]
pub fn spherical_to_cart_batch(spherical: &ArrayView2<'_, f64>) -> SpatialResult<Array2<f64>> {
if spherical.ncols() != 3 {
return Err(SpatialError::DimensionError(format!(
"Spherical coordinates must have 3 columns, got {}",
spherical.ncols()
)));
}
let n_points = spherical.nrows();
let mut result = Array2::zeros((n_points, 3));
for (i, row) in spherical.rows().into_iter().enumerate() {
let cart = spherical_to_cart(&row)?;
result.row_mut(i).assign(&cart);
}
Ok(result)
}
#[allow(dead_code)]
pub fn geodesic_distance(
spherical1: &ArrayView1<f64>,
spherical2: &ArrayView1<f64>,
) -> SpatialResult<f64> {
if spherical1.len() != 3 || spherical2.len() != 3 {
return Err(SpatialError::DimensionError(
"Spherical coordinates must have 3 elements".into(),
));
}
let r1 = spherical1[0];
let theta1 = spherical1[1];
let phi1 = spherical1[2];
let r2 = spherical2[0];
let theta2 = spherical2[1];
let phi2 = spherical2[2];
if r1 < 0.0 || r2 < 0.0 {
return Err(SpatialError::ValueError(
"Radius must be non-negative".into(),
));
}
if (r1 - r2).abs() > 1e-10 {
return Err(SpatialError::ValueError(
"Geodesic distance is only defined for points on the same sphere".into(),
));
}
let cos_theta1 = theta1.cos();
let sin_theta1 = theta1.sin();
let cos_theta2 = theta2.cos();
let sin_theta2 = theta2.sin();
let cos_dphi = (phi1 - phi2).cos();
let cos_angle = cos_theta1 * cos_theta2 + sin_theta1 * sin_theta2 * cos_dphi;
let cos_angle = cos_angle.clamp(-1.0, 1.0);
let angle = cos_angle.acos();
let distance = r1 * angle;
Ok(distance)
}
#[allow(dead_code)]
pub fn spherical_triangle_area(
p1: &ArrayView1<f64>,
p2: &ArrayView1<f64>,
p3: &ArrayView1<f64>,
) -> SpatialResult<f64> {
let r1 = p1[0];
let r2 = p2[0];
let r3 = p3[0];
let cart1 = spherical_to_cart(p1)?;
let cart2 = spherical_to_cart(p2)?;
let cart3 = spherical_to_cart(p3)?;
let v1 = &cart1 / r1;
let v2 = &cart2 / r2;
let v3 = &cart3 / r3;
let dot12 = v1.dot(&v2);
let dot23 = v2.dot(&v3);
let dot31 = v3.dot(&v1);
let dot12 = dot12.clamp(-1.0, 1.0);
let dot23 = dot23.clamp(-1.0, 1.0);
let dot31 = dot31.clamp(-1.0, 1.0);
let a12 = dot12.acos();
let a23 = dot23.acos();
let a31 = dot31.acos();
let excess = a12 + a23 + a31 - PI;
let area = r1 * r1 * excess;
Ok(area)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_cart_to_spherical() {
let cart = array![0.0, 0.0, 0.0];
let spherical = cart_to_spherical(&cart.view()).expect("Operation failed");
assert_relative_eq!(spherical[0], 0.0);
assert_relative_eq!(spherical[1], 0.0);
assert_relative_eq!(spherical[2], 0.0);
let cart = array![1.0, 0.0, 0.0];
let spherical = cart_to_spherical(&cart.view()).expect("Operation failed");
assert_relative_eq!(spherical[0], 1.0); assert_relative_eq!(spherical[1], PI / 2.0); assert_relative_eq!(spherical[2], 0.0);
let cart = array![0.0, 1.0, 0.0];
let spherical = cart_to_spherical(&cart.view()).expect("Operation failed");
assert_relative_eq!(spherical[0], 1.0); assert_relative_eq!(spherical[1], PI / 2.0); assert_relative_eq!(spherical[2], PI / 2.0);
let cart = array![0.0, 0.0, 1.0];
let spherical = cart_to_spherical(&cart.view()).expect("Operation failed");
assert_relative_eq!(spherical[0], 1.0); assert_relative_eq!(spherical[1], 0.0); assert_relative_eq!(spherical[2], 0.0);
let cart = array![1.0, 1.0, 1.0];
let spherical = cart_to_spherical(&cart.view()).expect("Operation failed");
assert_relative_eq!(spherical[0], 3.0_f64.sqrt(), epsilon = 1e-6); assert_relative_eq!(spherical[1], (1.0 / 3.0_f64.sqrt()).acos(), epsilon = 1e-6); assert_relative_eq!(spherical[2], PI / 4.0, epsilon = 1e-6); }
#[test]
fn test_spherical_to_cart() {
let spherical = array![0.0, 0.0, 0.0];
let cart = spherical_to_cart(&spherical.view()).expect("Operation failed");
assert_relative_eq!(cart[0], 0.0);
assert_relative_eq!(cart[1], 0.0);
assert_relative_eq!(cart[2], 0.0);
let spherical = array![1.0, PI / 2.0, 0.0];
let cart = spherical_to_cart(&spherical.view()).expect("Operation failed");
assert_relative_eq!(cart[0], 1.0, epsilon = 1e-6);
assert_relative_eq!(cart[1], 0.0, epsilon = 1e-6);
assert_relative_eq!(cart[2], 0.0, epsilon = 1e-6);
let spherical = array![1.0, PI / 2.0, PI / 2.0];
let cart = spherical_to_cart(&spherical.view()).expect("Operation failed");
assert_relative_eq!(cart[0], 0.0, epsilon = 1e-6);
assert_relative_eq!(cart[1], 1.0, epsilon = 1e-6);
assert_relative_eq!(cart[2], 0.0, epsilon = 1e-6);
let spherical = array![1.0, 0.0, 0.0];
let cart = spherical_to_cart(&spherical.view()).expect("Operation failed");
assert_relative_eq!(cart[0], 0.0, epsilon = 1e-6);
assert_relative_eq!(cart[1], 0.0, epsilon = 1e-6);
assert_relative_eq!(cart[2], 1.0, epsilon = 1e-6);
let spherical = array![2.0, PI / 4.0, PI / 3.0];
let cart = spherical_to_cart(&spherical.view()).expect("Operation failed");
assert_relative_eq!(
cart[0],
2.0 * (PI / 4.0).sin() * (PI / 3.0).cos(),
epsilon = 1e-6
);
assert_relative_eq!(
cart[1],
2.0 * (PI / 4.0).sin() * (PI / 3.0).sin(),
epsilon = 1e-6
);
assert_relative_eq!(cart[2], 2.0 * (PI / 4.0).cos(), epsilon = 1e-6);
}
#[test]
fn test_roundtrip() {
let cart_points = array![
[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, 1.0], [3.0, -2.0, 4.0], ];
for row in cart_points.rows() {
let cart_original = row.to_owned();
let spherical = cart_to_spherical(&cart_original.view()).expect("Operation failed");
let cart_roundtrip = spherical_to_cart(&spherical.view()).expect("Operation failed");
for i in 0..3 {
assert_relative_eq!(cart_original[i], cart_roundtrip[i], epsilon = 1e-6);
}
}
}
#[test]
fn test_batch_conversions() {
let cart = array![
[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ];
let spherical = cart_to_spherical_batch(&cart.view()).expect("Operation failed");
assert_eq!(spherical.shape(), &[3, 3]);
assert_relative_eq!(spherical[[0, 0]], 1.0); assert_relative_eq!(spherical[[0, 1]], PI / 2.0); assert_relative_eq!(spherical[[0, 2]], 0.0);
let cart_roundtrip = spherical_to_cart_batch(&spherical.view()).expect("Operation failed");
assert_eq!(cart_roundtrip.shape(), &[3, 3]);
for i in 0..3 {
for j in 0..3 {
assert_relative_eq!(cart[[i, j]], cart_roundtrip[[i, j]], epsilon = 1e-6);
}
}
}
#[test]
fn test_geodesic_distance() {
let north_pole = array![1.0, 0.0, 0.0];
let equator_point = array![1.0, PI / 2.0, 0.0];
let distance =
geodesic_distance(&north_pole.view(), &equator_point.view()).expect("Operation failed");
assert_relative_eq!(distance, PI / 2.0, epsilon = 1e-6);
let point1 = array![1.0, PI / 2.0, 0.0];
let point2 = array![1.0, PI / 2.0, PI];
let distance = geodesic_distance(&point1.view(), &point2.view()).expect("Operation failed");
assert_relative_eq!(distance, PI, epsilon = 1e-6);
let point1 = array![2.0, PI / 2.0, 0.0];
let point2 = array![2.0, PI / 2.0, PI / 3.0];
let distance = geodesic_distance(&point1.view(), &point2.view()).expect("Operation failed");
assert_relative_eq!(distance, 2.0 * PI / 3.0, epsilon = 1e-6); }
#[test]
fn test_spherical_triangle_area() {
let p1 = array![1.0, 0.0, 0.0]; let p2 = array![1.0, PI / 2.0, 0.0]; let p3 = array![1.0, PI / 2.0, PI / 2.0];
let area =
spherical_triangle_area(&p1.view(), &p2.view(), &p3.view()).expect("Operation failed");
assert_relative_eq!(area, PI / 2.0, epsilon = 1e-6);
let p1 = array![1.0, 0.0, 0.0]; let p2 = array![1.0, PI, 0.0]; let p3 = array![1.0, PI / 2.0, 0.0];
let area =
spherical_triangle_area(&p1.view(), &p2.view(), &p3.view()).expect("Operation failed");
assert_relative_eq!(area, PI, epsilon = 1e-6); }
#[test]
fn test_error_cases() {
let bad_cart = array![1.0, 2.0];
assert!(cart_to_spherical(&bad_cart.view()).is_err());
let bad_spherical = array![1.0, 2.0];
assert!(spherical_to_cart(&bad_spherical.view()).is_err());
let neg_radius = array![-1.0, PI / 2.0, 0.0];
assert!(spherical_to_cart(&neg_radius.view()).is_err());
let bad_theta = array![1.0, -0.1, 0.0];
assert!(spherical_to_cart(&bad_theta.view()).is_err());
let bad_theta = array![1.0, PI + 0.1, 0.0];
assert!(spherical_to_cart(&bad_theta.view()).is_err());
let p1 = array![1.0, 0.0, 0.0];
let p2 = array![2.0, 0.0, 0.0];
assert!(geodesic_distance(&p1.view(), &p2.view()).is_err());
}
}