use crate::descriptor::{SpaceDescriptor, from_nat, to_nat};
use crate::error::RankAdapterError;
use crate::metric;
use num_bigint::BigUint;
use sim_lib_discrete_comb::{mixed_radix_rank, mixed_radix_unrank};
use sim_lib_rank::Nat;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FwhtSignalSpace {
pub len: usize,
pub alphabet: u64,
}
impl FwhtSignalSpace {
fn radices(&self) -> Vec<u64> {
vec![self.alphabet; self.len]
}
pub fn descriptor(&self) -> SpaceDescriptor {
SpaceDescriptor {
id: "rank/discrete/fwht-signal",
version: 1,
params: vec![
("len", self.len.to_string()),
("alphabet", self.alphabet.to_string()),
],
order: "mixed-radix",
metric: "coefficient-l1",
}
}
pub fn cardinality(&self) -> Nat {
to_nat(BigUint::from(self.alphabet).pow(self.len as u32))
}
pub fn rank(&self, coeffs: &[i64]) -> Result<Nat, RankAdapterError> {
if coeffs.len() != self.len {
return Err(RankAdapterError::Invalid(format!(
"signal length {} != {}",
coeffs.len(),
self.len
)));
}
let mut digits = Vec::with_capacity(self.len);
for &c in coeffs {
if c < 0 || c as u64 >= self.alphabet {
return Err(RankAdapterError::Invalid(format!(
"coefficient {c} outside alphabet 0..{}",
self.alphabet
)));
}
digits.push(c as u64);
}
Ok(to_nat(mixed_radix_rank(&digits, &self.radices())?))
}
pub fn unrank(&self, ordinal: &Nat) -> Result<Vec<i64>, RankAdapterError> {
let digits = mixed_radix_unrank(&from_nat(ordinal), &self.radices())?;
Ok(digits.into_iter().map(|d| d as i64).collect())
}
pub fn distance(&self, a: &Nat, b: &Nat) -> Result<Nat, RankAdapterError> {
Ok(to_nat(metric::l1_i64(&self.unrank(a)?, &self.unrank(b)?)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nat(i: u32) -> Nat {
to_nat(BigUint::from(i))
}
#[test]
fn round_trips_and_cardinality() {
let space = FwhtSignalSpace {
len: 3,
alphabet: 2,
};
assert_eq!(space.cardinality(), nat(8));
for i in 0..8u32 {
let s = space.unrank(&nat(i)).unwrap();
assert_eq!(space.rank(&s).unwrap(), nat(i));
}
}
#[test]
fn coefficient_l1_distance() {
let space = FwhtSignalSpace {
len: 3,
alphabet: 4,
};
let a = space.rank(&[3, 0, 1]).unwrap();
let b = space.rank(&[0, 2, 1]).unwrap();
assert_eq!(space.distance(&a, &b).unwrap(), nat(3 + 2));
}
#[test]
fn out_of_alphabet_rejected() {
let space = FwhtSignalSpace {
len: 2,
alphabet: 2,
};
assert!(matches!(
space.rank(&[5, 0]),
Err(RankAdapterError::Invalid(_))
));
}
}