use crate::error::{ensure_finite, ensure_finite_output};
use crate::{Matrix3, PykepError, Result, Vector3};
fn validate_vector(parameter: &'static str, vector: &Vector3) -> Result<()> {
for &value in vector {
ensure_finite(parameter, value)?;
}
Ok(())
}
pub fn dot(left: &Vector3, right: &Vector3) -> Result<f64> {
validate_vector("left", left)?;
validate_vector("right", right)?;
ensure_finite_output(
"dot",
left[0].mul_add(right[0], left[1].mul_add(right[1], left[2] * right[2])),
)
}
pub fn norm(vector: &Vector3) -> Result<f64> {
validate_vector("vector", vector)?;
ensure_finite_output("norm", vector[0].hypot(vector[1]).hypot(vector[2]))
}
pub fn normalize(vector: &Vector3) -> Result<Vector3> {
let magnitude = norm(vector)?;
if magnitude == 0.0 {
return Err(PykepError::SingularGeometry {
operation: "normalize",
});
}
let result = [
vector[0] / magnitude,
vector[1] / magnitude,
vector[2] / magnitude,
];
validate_vector("normalized_vector", &result)?;
Ok(result)
}
pub fn cross(left: &Vector3, right: &Vector3) -> Result<Vector3> {
validate_vector("left", left)?;
validate_vector("right", right)?;
let result = [
left[1].mul_add(right[2], -(left[2] * right[1])),
left[2].mul_add(right[0], -(left[0] * right[2])),
left[0].mul_add(right[1], -(left[1] * right[0])),
];
for &value in &result {
ensure_finite_output("cross", value)?;
}
Ok(result)
}
pub fn skew(vector: &Vector3) -> Result<Matrix3> {
validate_vector("vector", vector)?;
Ok([
[0.0, -vector[2], vector[1]],
[vector[2], 0.0, -vector[0]],
[-vector[1], vector[0], 0.0],
])
}
fn paired_batch<R>(
left: &[Vector3],
right: &[Vector3],
workers: usize,
operation: fn(&Vector3, &Vector3) -> Result<R>,
) -> Result<Vec<R>>
where
R: Send,
{
if left.len() != right.len() {
return Err(PykepError::DimensionMismatch {
expected: left.len(),
actual: right.len(),
});
}
let inputs: Vec<_> = left.iter().zip(right).collect();
crate::batch::try_map(&inputs, workers, |(left, right)| operation(left, right))
}
pub fn dot_batch(left: &[Vector3], right: &[Vector3], workers: usize) -> Result<Vec<f64>> {
paired_batch(left, right, workers, dot)
}
pub fn norm_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<f64>> {
crate::batch::try_map(vectors, workers, norm)
}
pub fn normalize_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<Vector3>> {
crate::batch::try_map(vectors, workers, normalize)
}
pub fn cross_batch(left: &[Vector3], right: &[Vector3], workers: usize) -> Result<Vec<Vector3>> {
paired_batch(left, right, workers, cross)
}
pub fn skew_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<Matrix3>> {
crate::batch::try_map(vectors, workers, skew)
}
#[must_use]
pub fn identity<const N: usize>() -> [[f64; N]; N] {
let mut result = [[0.0; N]; N];
for (index, row) in result.iter_mut().enumerate() {
row[index] = 1.0;
}
result
}
pub fn matrix_multiply<const M: usize, const K: usize, const N: usize>(
left: &[[f64; K]; M],
right: &[[f64; N]; K],
) -> Result<[[f64; N]; M]> {
let mut result = [[0.0; N]; M];
for row in 0..M {
for column in 0..N {
let mut value = 0.0;
for inner in 0..K {
ensure_finite("left", left[row][inner])?;
ensure_finite("right", right[inner][column])?;
value = left[row][inner].mul_add(right[inner][column], value);
}
result[row][column] = ensure_finite_output("matrix_multiply", value)?;
}
}
Ok(result)
}
pub fn matrix_vector_multiply<const M: usize, const N: usize>(
matrix: &[[f64; N]; M],
vector: &[f64; N],
) -> Result<[f64; M]> {
let mut result = [0.0; M];
for row in 0..M {
let mut value = 0.0;
for column in 0..N {
ensure_finite("matrix", matrix[row][column])?;
ensure_finite("vector", vector[column])?;
value = matrix[row][column].mul_add(vector[column], value);
}
result[row] = ensure_finite_output("matrix_vector_multiply", value)?;
}
Ok(result)
}
#[must_use]
pub fn transpose<const M: usize, const N: usize>(matrix: &[[f64; N]; M]) -> [[f64; M]; N] {
let mut result = [[0.0; M]; N];
for (row, values) in matrix.iter().enumerate() {
for (column, &value) in values.iter().enumerate() {
result[column][row] = value;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vector_operations_match_right_handed_geometry() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 1.0, 0.0];
assert_eq!(dot(&x, &y), Ok(0.0));
assert_eq!(norm(&[3.0, 4.0, 12.0]), Ok(13.0));
assert_eq!(cross(&x, &y), Ok([0.0, 0.0, 1.0]));
assert_eq!(
matrix_vector_multiply(&skew(&x).unwrap(), &y),
cross(&x, &y)
);
}
#[test]
fn normalization_is_stable_for_large_finite_vectors() {
let value = normalize(&[1e308, 1e308, 0.0]).unwrap();
let expected = core::f64::consts::FRAC_1_SQRT_2;
assert!((value[0] - expected).abs() < 2e-16);
assert!((value[1] - expected).abs() < 2e-16);
}
#[test]
fn zero_and_non_finite_vectors_are_rejected() {
assert!(matches!(
normalize(&[0.0; 3]),
Err(PykepError::SingularGeometry { .. })
));
assert!(dot(&[f64::NAN, 0.0, 0.0], &[1.0; 3]).is_err());
}
#[test]
fn fixed_matrix_operations_preserve_layout() {
let left = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let right = [[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]];
assert_eq!(
matrix_multiply(&left, &right),
Ok([[58.0, 64.0], [139.0, 154.0]])
);
assert_eq!(transpose(&left), [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]);
assert_eq!(
identity::<3>(),
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
);
}
#[test]
fn ordered_vector_batches_match_scalar_operations() {
let left = [[1.0, 2.0, 3.0], [-2.0, 0.5, 4.0]];
let right = [[4.0, -1.0, 0.5], [1.0, 3.0, -2.0]];
assert_eq!(
dot_batch(&left, &right, 2).unwrap(),
left.iter()
.zip(&right)
.map(|(left, right)| dot(left, right).unwrap())
.collect::<Vec<_>>()
);
assert_eq!(
norm_batch(&left, 2).unwrap(),
left.iter()
.map(|value| norm(value).unwrap())
.collect::<Vec<_>>()
);
assert_eq!(
normalize_batch(&left, 2).unwrap(),
left.iter()
.map(|value| normalize(value).unwrap())
.collect::<Vec<_>>()
);
assert_eq!(
cross_batch(&left, &right, 2).unwrap(),
left.iter()
.zip(&right)
.map(|(left, right)| cross(left, right).unwrap())
.collect::<Vec<_>>()
);
assert_eq!(
skew_batch(&left, 2).unwrap(),
left.iter()
.map(|value| skew(value).unwrap())
.collect::<Vec<_>>()
);
assert!(dot_batch(&left, &right[..1], 2).is_err());
}
}