use nebu::Goldilocks;
pub const EXPANSION: usize = 2;
const BASE_DEGREE: usize = 6;
const WALK_LENGTH: usize = 2;
#[cfg(test)]
const DEGREE: usize = BASE_DEGREE * BASE_DEGREE;
pub struct Expander {
pub n: usize,
pub m: usize,
p: usize,
}
impl Expander {
pub fn new(n: usize) -> Self {
assert!(n > 0, "expander requires n > 0");
let m = EXPANSION * n;
let p = Self::smallest_prime_with_square_ge(m);
Self { n, m, p }
}
fn smallest_prime_with_square_ge(target: usize) -> usize {
let sqrt = (target as f64).sqrt().ceil() as usize;
let mut p = if sqrt < 2 { 2 } else { sqrt };
while !Self::is_prime(p) {
p += 1;
}
p
}
fn is_prime(n: usize) -> bool {
if n < 2 {
return false;
}
if n < 4 {
return true;
}
if n.is_multiple_of(2) || n.is_multiple_of(3) {
return false;
}
let mut i = 5;
while i * i <= n {
if n.is_multiple_of(i) || n.is_multiple_of(i + 2) {
return false;
}
i += 6;
}
true
}
#[inline]
fn margulis_neighbors(&self, x: usize, y: usize) -> [(usize, usize); BASE_DEGREE] {
let p = self.p;
[
((x + y) % p, y),
((x + p - y) % p, y),
(x, (y + x) % p),
(x, (y + p - x) % p),
((x + y + 1) % p, y),
(x, (y + x + 1) % p),
]
}
#[inline]
fn to_index(&self, x: usize, y: usize) -> usize {
(x * self.p + y) % self.m
}
#[inline]
fn index_to_2d(&self, i: usize) -> (usize, usize) {
(i / self.p % self.p, i % self.p)
}
pub fn neighbors(&self, i: usize) -> Vec<usize> {
let (x, y) = self.index_to_2d(i);
let mut current: Vec<(usize, usize)> = self.margulis_neighbors(x, y).to_vec();
for _ in 1..WALK_LENGTH {
let mut next = Vec::with_capacity(current.len() * BASE_DEGREE);
for &(cx, cy) in ¤t {
next.extend_from_slice(&self.margulis_neighbors(cx, cy));
}
current = next;
}
current.iter().map(|&(x, y)| self.to_index(x, y)).collect()
}
pub fn encode(&self, input: &[Goldilocks]) -> Vec<Goldilocks> {
assert_eq!(input.len(), self.n);
let mut output = vec![Goldilocks::ZERO; self.m];
for (i, &val) in input.iter().enumerate() {
for r in self.neighbors(i) {
output[r] += val;
}
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smallest_prime() {
assert_eq!(Expander::smallest_prime_with_square_ge(4), 2); assert_eq!(Expander::smallest_prime_with_square_ge(5), 3); assert_eq!(Expander::smallest_prime_with_square_ge(100), 11); }
#[test]
fn expander_deterministic() {
let exp = Expander::new(16);
let n1 = exp.neighbors(5);
let n2 = exp.neighbors(5);
assert_eq!(n1, n2);
}
#[test]
fn correct_degree() {
let exp = Expander::new(64);
let neighbors = exp.neighbors(7);
assert_eq!(neighbors.len(), DEGREE);
}
#[test]
fn neighbors_in_range() {
let exp = Expander::new(64);
for i in 0..64 {
for &r in &exp.neighbors(i) {
assert!(r < exp.m, "neighbor {r} out of range [0, {})", exp.m);
}
}
}
#[test]
fn expansion_property() {
let exp = Expander::new(64);
let mut all_neighbors = std::collections::HashSet::new();
for i in 0..10 {
for &r in &exp.neighbors(i) {
all_neighbors.insert(r);
}
}
assert!(
all_neighbors.len() > exp.m / 3,
"expansion too low: {} distinct out of {}",
all_neighbors.len(),
exp.m
);
}
#[test]
fn encode_zero_polynomial() {
let exp = Expander::new(8);
let input = vec![Goldilocks::ZERO; 8];
let output = exp.encode(&input);
for &v in &output {
assert_eq!(v, Goldilocks::ZERO);
}
}
#[test]
fn encode_output_size() {
let exp = Expander::new(16);
let input = vec![Goldilocks::ONE; 16];
let output = exp.encode(&input);
assert_eq!(output.len(), EXPANSION * 16);
}
#[test]
fn encode_different_inputs_differ() {
let exp = Expander::new(8);
let a = vec![Goldilocks::ONE; 8];
let mut b = vec![Goldilocks::ONE; 8];
b[0] = Goldilocks::new(42);
assert_ne!(exp.encode(&a), exp.encode(&b));
}
}