#[inline]
#[must_use]
pub const fn blade_grade(blade_index: usize) -> u32 {
blade_index.count_ones()
}
#[must_use]
pub fn blades_at_grade_count(dimension: u32, grade: u32) -> usize {
if grade > dimension {
return 0;
}
binomial(dimension, grade)
}
fn binomial(n: u32, k: u32) -> usize {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k) as usize;
let mut result: usize = 1;
for i in 0..k {
result = result * (n as usize - i) / (i + 1);
}
result
}
#[must_use]
pub fn grade_indices(dimension: u32, grade: u32) -> Vec<usize> {
let total = 1usize << dimension;
(0..total).filter(|i| blade_grade(*i) == grade).collect()
}
#[must_use]
pub fn grade_extract(dimension: u32, grade: u32, coefficients: &[f64]) -> Vec<f64> {
grade_indices(dimension, grade)
.iter()
.filter_map(|&i| coefficients.get(i).copied())
.collect()
}
#[must_use]
pub fn grade_project(dimension: u32, grade: u32, coefficients: &[f64]) -> Vec<f64> {
let total = 1usize << dimension;
let mut result = vec![0.0; total.min(coefficients.len())];
for (i, coeff) in coefficients.iter().enumerate().take(total) {
if blade_grade(i) == grade {
result[i] = *coeff;
}
}
result
}
#[must_use]
pub fn grade_project_max(dimension: u32, max_grade: u32, coefficients: &[f64]) -> Vec<f64> {
let total = 1usize << dimension;
let mut result = coefficients.to_vec();
result.resize(total, 0.0);
for (i, coeff) in result.iter_mut().enumerate().take(total) {
if blade_grade(i) > max_grade {
*coeff = 0.0;
}
}
result.truncate(coefficients.len());
result
}
#[must_use]
pub fn grade_mask(dimension: u32, coefficients: &[f64]) -> u32 {
let total = 1usize << dimension;
let mut mask = 0u32;
for (i, coeff) in coefficients.iter().enumerate().take(total) {
if *coeff != 0.0 {
mask |= 1 << blade_grade(i);
}
}
mask
}
#[must_use]
pub fn has_grade(dimension: u32, grade: u32, coefficients: &[f64]) -> bool {
grade_mask(dimension, coefficients) & (1 << grade) != 0
}
#[must_use]
pub fn is_pure_grade(dimension: u32, coefficients: &[f64]) -> bool {
let mask = grade_mask(dimension, coefficients);
mask.count_ones() <= 1
}
#[must_use]
pub fn component_get(coefficients: &[f64], blade_index: usize) -> Option<f64> {
coefficients.get(blade_index).copied()
}
#[must_use]
pub fn component_set(coefficients: &[f64], blade_index: usize, value: f64) -> Vec<f64> {
let mut result = coefficients.to_vec();
if blade_index < result.len() {
result[blade_index] = value;
}
result
}
#[must_use]
pub fn norm(coefficients: &[f64]) -> f64 {
coefficients.iter().map(|c| c * c).sum::<f64>().sqrt()
}
#[must_use]
pub fn norm_squared(coefficients: &[f64]) -> f64 {
coefficients.iter().map(|c| c * c).sum()
}
#[must_use]
pub fn normalize(coefficients: &[f64]) -> Option<Vec<f64>> {
let n = norm(coefficients);
if n < f64::EPSILON {
return None;
}
Some(coefficients.iter().map(|c| c / n).collect())
}
#[must_use]
pub fn grade_involution(dimension: u32, coefficients: &[f64]) -> Vec<f64> {
let total = 1usize << dimension;
coefficients
.iter()
.enumerate()
.take(total)
.map(|(i, c)| if blade_grade(i) % 2 == 1 { -c } else { *c })
.collect()
}
#[must_use]
pub fn reverse(dimension: u32, coefficients: &[f64]) -> Vec<f64> {
let total = 1usize << dimension;
coefficients
.iter()
.enumerate()
.take(total)
.map(|(i, c)| {
let k = blade_grade(i);
if k > 0 && (k * (k - 1) / 2) % 2 == 1 {
-c
} else {
*c
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_3d() -> Vec<f64> {
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
}
#[test]
fn test_blade_grade_scalar() {
assert_eq!(blade_grade(0), 0);
}
#[test]
fn test_blade_grade_vectors() {
assert_eq!(blade_grade(0b001), 1); assert_eq!(blade_grade(0b010), 1); assert_eq!(blade_grade(0b100), 1); }
#[test]
fn test_blade_grade_bivectors() {
assert_eq!(blade_grade(0b011), 2); assert_eq!(blade_grade(0b101), 2); assert_eq!(blade_grade(0b110), 2); }
#[test]
fn test_blade_grade_trivector() {
assert_eq!(blade_grade(0b111), 3); }
#[test]
fn test_blades_at_grade_count_3d() {
assert_eq!(blades_at_grade_count(3, 0), 1);
assert_eq!(blades_at_grade_count(3, 1), 3);
assert_eq!(blades_at_grade_count(3, 2), 3);
assert_eq!(blades_at_grade_count(3, 3), 1);
assert_eq!(blades_at_grade_count(3, 4), 0);
}
#[test]
fn test_blades_at_grade_count_4d() {
assert_eq!(blades_at_grade_count(4, 0), 1);
assert_eq!(blades_at_grade_count(4, 1), 4);
assert_eq!(blades_at_grade_count(4, 2), 6);
assert_eq!(blades_at_grade_count(4, 3), 4);
assert_eq!(blades_at_grade_count(4, 4), 1);
}
#[test]
fn test_grade_indices_3d() {
assert_eq!(grade_indices(3, 0), vec![0]);
assert_eq!(grade_indices(3, 1), vec![1, 2, 4]);
assert_eq!(grade_indices(3, 2), vec![3, 5, 6]);
assert_eq!(grade_indices(3, 3), vec![7]);
}
#[test]
fn test_grade_extract_scalar() {
assert_eq!(grade_extract(3, 0, &sample_3d()), vec![1.0]);
}
#[test]
fn test_grade_extract_vectors() {
assert_eq!(grade_extract(3, 1, &sample_3d()), vec![2.0, 3.0, 5.0]);
}
#[test]
fn test_grade_extract_bivectors() {
assert_eq!(grade_extract(3, 2, &sample_3d()), vec![4.0, 6.0, 7.0]);
}
#[test]
fn test_grade_extract_trivector() {
assert_eq!(grade_extract(3, 3, &sample_3d()), vec![8.0]);
}
#[test]
fn test_grade_extract_empty() {
assert_eq!(grade_extract(3, 4, &sample_3d()), Vec::<f64>::new());
}
#[test]
fn test_grade_project_vectors_only() {
let projected = grade_project(3, 1, &sample_3d());
assert_eq!(projected, vec![0.0, 2.0, 3.0, 0.0, 5.0, 0.0, 0.0, 0.0]);
}
#[test]
fn test_grade_project_scalar_only() {
let projected = grade_project(3, 0, &sample_3d());
assert_eq!(projected, vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn test_grade_project_idempotent() {
let once = grade_project(3, 1, &sample_3d());
let twice = grade_project(3, 1, &once);
assert_eq!(once, twice);
}
#[test]
fn test_grade_project_max() {
let projected = grade_project_max(3, 1, &sample_3d());
assert_eq!(projected, vec![1.0, 2.0, 3.0, 0.0, 5.0, 0.0, 0.0, 0.0]);
}
#[test]
fn test_grade_project_max_all() {
let projected = grade_project_max(3, 3, &sample_3d());
assert_eq!(projected, sample_3d());
}
#[test]
fn test_grade_mask_all_grades() {
assert_eq!(grade_mask(3, &sample_3d()), 0b1111);
}
#[test]
fn test_grade_mask_scalar_only() {
let scalar = vec![5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
assert_eq!(grade_mask(3, &scalar), 0b0001);
}
#[test]
fn test_grade_mask_bivector_only() {
let bv = vec![0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 3.0, 0.0];
assert_eq!(grade_mask(3, &bv), 0b0100);
}
#[test]
fn test_grade_mask_zero() {
let zero = vec![0.0; 8];
assert_eq!(grade_mask(3, &zero), 0);
}
#[test]
fn test_has_grade() {
let vectors = vec![0.0, 1.0, 2.0, 0.0, 3.0, 0.0, 0.0, 0.0];
assert!(!has_grade(3, 0, &vectors));
assert!(has_grade(3, 1, &vectors));
assert!(!has_grade(3, 2, &vectors));
}
#[test]
fn test_is_pure_grade_vector() {
let vector = vec![0.0, 1.0, 2.0, 0.0, 3.0, 0.0, 0.0, 0.0];
assert!(is_pure_grade(3, &vector));
}
#[test]
fn test_is_pure_grade_mixed() {
assert!(!is_pure_grade(3, &sample_3d()));
}
#[test]
fn test_is_pure_grade_zero() {
let zero = vec![0.0; 8];
assert!(is_pure_grade(3, &zero));
}
#[test]
fn test_component_get() {
let mv = sample_3d();
assert_eq!(component_get(&mv, 0), Some(1.0));
assert_eq!(component_get(&mv, 3), Some(4.0));
assert_eq!(component_get(&mv, 10), None);
}
#[test]
fn test_component_set() {
let mv = sample_3d();
let updated = component_set(&mv, 1, 99.0);
assert_eq!(updated[1], 99.0);
assert_eq!(updated[0], 1.0); assert_eq!(updated.len(), mv.len());
}
#[test]
fn test_norm_345() {
assert!((norm(&[3.0, 4.0]) - 5.0).abs() < 1e-10);
}
#[test]
fn test_norm_zero() {
assert!((norm(&[0.0, 0.0]) - 0.0).abs() < 1e-10);
}
#[test]
fn test_norm_squared() {
assert!((norm_squared(&[3.0, 4.0]) - 25.0).abs() < 1e-10);
}
#[test]
fn test_normalize() {
let unit = normalize(&[3.0, 4.0]).unwrap();
assert!((norm(&unit) - 1.0).abs() < 1e-10);
assert!((unit[0] - 0.6).abs() < 1e-10);
assert!((unit[1] - 0.8).abs() < 1e-10);
}
#[test]
fn test_normalize_zero() {
assert!(normalize(&[0.0, 0.0]).is_none());
}
#[test]
fn test_grade_involution() {
let inv = grade_involution(3, &sample_3d());
assert_eq!(inv, vec![1.0, -2.0, -3.0, 4.0, -5.0, 6.0, 7.0, -8.0]);
}
#[test]
fn test_grade_involution_involution() {
let mv = sample_3d();
let double = grade_involution(3, &grade_involution(3, &mv));
for (a, b) in mv.iter().zip(double.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_reverse() {
let rev = reverse(3, &sample_3d());
assert_eq!(rev, vec![1.0, 2.0, 3.0, -4.0, 5.0, -6.0, -7.0, -8.0]);
}
#[test]
fn test_reverse_involution() {
let mv = sample_3d();
let double = reverse(3, &reverse(3, &mv));
for (a, b) in mv.iter().zip(double.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_extract_then_norm() {
let mv = sample_3d();
let bivectors = grade_extract(3, 2, &mv);
let bv_norm = norm(&bivectors);
assert!((bv_norm - 101.0f64.sqrt()).abs() < 1e-10);
}
#[test]
fn test_project_preserves_grade_mask() {
let mv = sample_3d();
let projected = grade_project(3, 2, &mv);
assert_eq!(grade_mask(3, &projected), 0b0100);
}
#[test]
fn test_grade_filter_pattern() {
let mvs = [
vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], vec![0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0], ];
let with_bivectors: Vec<_> = mvs.iter().filter(|mv| has_grade(3, 2, mv)).collect();
assert_eq!(with_bivectors.len(), 2);
}
#[test]
fn test_binomial_sum() {
for n in 0..8 {
let sum: usize = (0..=n).map(|k| blades_at_grade_count(n, k)).sum();
assert_eq!(sum, 1 << n);
}
}
#[test]
fn test_2d_algebra() {
let mv = vec![1.0, 2.0, 3.0, 4.0];
assert_eq!(grade_indices(2, 0), vec![0]);
assert_eq!(grade_indices(2, 1), vec![1, 2]);
assert_eq!(grade_indices(2, 2), vec![3]);
assert_eq!(grade_extract(2, 1, &mv), vec![2.0, 3.0]);
}
#[test]
fn test_4d_grade_indices() {
let indices = grade_indices(4, 2);
assert_eq!(indices.len(), 6);
for idx in &indices {
assert_eq!(blade_grade(*idx), 2);
}
}
#[cfg(not(target_arch = "wasm32"))]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn arbitrary_mv(dim: u32) -> impl Strategy<Value = Vec<f64>> {
let size = 1usize << dim;
proptest::collection::vec(-100.0f64..100.0, size)
}
proptest! {
#[test]
fn prop_grade_project_idempotent(mv in arbitrary_mv(3), grade in 0u32..4) {
let once = grade_project(3, grade, &mv);
let twice = grade_project(3, grade, &once);
for (a, b) in once.iter().zip(twice.iter()) {
prop_assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn prop_grade_extract_length(mv in arbitrary_mv(3), grade in 0u32..4) {
let extracted = grade_extract(3, grade, &mv);
prop_assert_eq!(extracted.len(), blades_at_grade_count(3, grade));
}
#[test]
fn prop_grade_mask_consistent(mv in arbitrary_mv(3)) {
let mask = grade_mask(3, &mv);
for grade in 0u32..4 {
let extracted = grade_extract(3, grade, &mv);
let has_nonzero = extracted.iter().any(|c| *c != 0.0);
prop_assert_eq!(
mask & (1 << grade) != 0,
has_nonzero,
"grade {} mask mismatch", grade
);
}
}
#[test]
fn prop_grade_involution_involution(mv in arbitrary_mv(3)) {
let double = grade_involution(3, &grade_involution(3, &mv));
for (a, b) in mv.iter().zip(double.iter()) {
prop_assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn prop_reverse_involution(mv in arbitrary_mv(3)) {
let double = reverse(3, &reverse(3, &mv));
for (a, b) in mv.iter().zip(double.iter()) {
prop_assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn prop_norm_non_negative(mv in arbitrary_mv(3)) {
prop_assert!(norm(&mv) >= 0.0);
}
#[test]
fn prop_normalize_unit(mv in arbitrary_mv(3)) {
if let Some(unit) = normalize(&mv) {
prop_assert!((norm(&unit) - 1.0).abs() < 1e-8);
}
}
}
}
}