polycore 0.1.6

Fields, prime fields, sparse and dense polynomials, and sparse echelon forms.
Documentation
use num_bigint::BigInt;
use num_rational::BigRational;
use polycore::Primes;
use polycore::crt::{self, CrtAccumulator, CrtError, MixedRadixAccumulator, WangContext};

#[test]
fn checked_updates_preserve_state_on_error() {
    let mut acc = CrtAccumulator::new(2);
    acc.add(5, &[1, 2]).unwrap();
    for (p, values, expected) in [
        (
            7,
            vec![1],
            CrtError::LengthMismatch {
                expected: 2,
                actual: 1,
            },
        ),
        (
            7,
            vec![1, 2, 3],
            CrtError::LengthMismatch {
                expected: 2,
                actual: 3,
            },
        ),
        (5, vec![1, 2], CrtError::NonCoprimeModuli),
        (1, vec![0, 0], CrtError::InvalidModulus),
        (7, vec![1, 7], CrtError::UnreducedResidue),
    ] {
        assert_eq!(acc.add(p, &values), Err(expected));
        assert_eq!(acc.residues(), &[BigInt::from(1), BigInt::from(2)]);
        assert_eq!(acc.modulus(), &BigInt::from(5));
        assert_eq!(acc.image_count(), 1);
    }
    let mut xs = vec![BigInt::from(1)];
    let mut m = BigInt::from(0);
    assert_eq!(
        crt::try_garner(&mut xs, &mut m, &[1], 7),
        Err(CrtError::InvalidModulus)
    );
    assert_eq!(xs, vec![BigInt::from(1)]);
    assert_eq!(m, BigInt::from(0));
}

#[test]
fn persistent_reconstruction_and_independent_verification() {
    let truth: Vec<_> = [(0, 1), (-7, 5), (123_456, 789), (1, 3)]
        .into_iter()
        .map(|(n, d)| BigRational::new(n.into(), d.into()))
        .collect();
    let mut acc = CrtAccumulator::new(truth.len());
    assert_eq!(acc.reconstruct(), None);
    for (i, p) in [101, 103, 107, 109, 113].into_iter().enumerate() {
        let values: Vec<_> = truth.iter().map(|q| crt::reduce(q, p).unwrap()).collect();
        acc.add(p, &values).unwrap();
        assert_eq!(acc.image_count(), i + 1);
        for (&v, x) in values.iter().zip(acc.residues()) {
            assert_eq!(crt::residue(x, p), v);
        }
        let _candidate = acc.reconstruct(); // An attempt must not consume accumulated state.
    }
    let got = acc.reconstruct().unwrap();
    assert_eq!(got, truth);
    for (a, b) in got.iter().zip(&truth) {
        assert_eq!(crt::reduce(a, 127), crt::reduce(b, 127));
    }
    acc.add(
        127,
        &truth
            .iter()
            .map(|q| crt::reduce(q, 127).unwrap())
            .collect::<Vec<_>>(),
    )
    .unwrap();
    assert_eq!(acc.reconstruct(), Some(truth));
}

#[test]
fn shared_wang_bounds_handle_signed_and_noncanonical_residues() {
    let m = BigInt::from(1009);
    let context = WangContext::new(&m).unwrap();
    for n in -12..=12 {
        for d in 1..=12 {
            let q = BigRational::new(n.into(), d.into());
            let x = BigInt::from(crt::reduce(&q, 1009).unwrap());
            for shifted in [&x - &m, x.clone(), &x + &m] {
                assert_eq!(context.reconstruct(&shifted), Some(q.clone()));
                assert_eq!(crt::wang(&shifted, &m), Some(q.clone()));
            }
        }
    }
    assert_eq!(context.reconstruct_many(&[]), Some(vec![]));
    assert_eq!(
        WangContext::new(&BigInt::from(5))
            .unwrap()
            .reconstruct_many(&[0.into(), 2.into()]),
        None
    );
    for m in [-1, 0, 1] {
        assert!(WangContext::new(&m.into()).is_none());
    }
}

#[test]
fn zero_width_accumulator_tracks_images() {
    let mut acc = CrtAccumulator::new(0);
    assert_eq!(acc.reconstruct(), None);
    acc.add(5, &[]).unwrap();
    assert_eq!(acc.reconstruct(), Some(vec![]));
    assert_eq!(acc.image_count(), 1);
}

#[test]
fn legacy_garner_rejects_lengths_before_mutation() {
    let mut xs = vec![BigInt::from(1), BigInt::from(2)];
    let mut m = BigInt::from(5);
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        crt::garner(&mut xs, &mut m, &[1], 7);
    }));
    assert!(result.is_err());
    assert_eq!(xs, vec![BigInt::from(1), BigInt::from(2)]);
    assert_eq!(m, BigInt::from(5));
}

#[test]
fn reconstruction_helpers_reject_inconsistent_lengths() {
    for voted in [false, true] {
        assert!(
            std::panic::catch_unwind(|| {
                let image = |p| Some(((), vec![0; if p == 5 { 2 } else { 1 }]));
                if voted {
                    crt::reconstruct_voted(|ps| ps.iter().map(|&p| image(p)).collect(), [5, 7]);
                } else {
                    crt::reconstruct_keyed(image, [5, 7]);
                }
            })
            .is_err()
        );
    }
    for len in [0, 2] {
        assert!(
            std::panic::catch_unwind(|| {
                crt::reconstruct_voted(|_| vec![Some(((), vec![0])); len], [5]);
            })
            .is_err()
        );
    }
}

#[test]
fn mixed_radix_updates_preserve_state_on_error() {
    let mut acc = MixedRadixAccumulator::new(2);
    acc.add(5, &[1, 2]).unwrap();
    for (p, values, expected) in [
        (
            7,
            vec![1],
            CrtError::LengthMismatch {
                expected: 2,
                actual: 1,
            },
        ),
        (5, vec![1, 2], CrtError::NonCoprimeModuli),
        (1, vec![0, 0], CrtError::InvalidModulus),
        (1 << 32, vec![0, 0], CrtError::InvalidModulus),
        (7, vec![1, 7], CrtError::UnreducedResidue),
    ] {
        assert_eq!(acc.add(p, &values), Err(expected));
        assert_eq!(acc.residues(), vec![BigInt::from(1), BigInt::from(2)]);
        assert_eq!(acc.primes(), &[5]);
    }
}

#[test]
fn mixed_radix_matches_garner_accumulator() {
    // Runs of shared denominators, then unrelated fractions, over 31-bit primes so that
    // several primes share one big radix digit and the digit sums are reduced eagerly.
    let truth: Vec<_> = (0..700i64)
        .map(|i| {
            let d = if i < 500 { 1 + i / 50 } else { 3 * i + 1 };
            BigRational::new((i * i - 40_000).pow(3).into(), d.pow(5).into())
        })
        .collect();
    let mut garner = CrtAccumulator::new(truth.len());
    let mut mixed = MixedRadixAccumulator::new(truth.len());
    let mut done = false;
    for p in Primes::below(1 << 31).take(12) {
        let values: Vec<_> = truth.iter().map(|q| crt::reduce(q, p).unwrap()).collect();
        garner.add(p, &values).unwrap();
        mixed.add(p, &values).unwrap();
        assert_eq!(mixed.residues(), garner.residues());
        assert_eq!(&mixed.modulus(), garner.modulus());
        assert_eq!(mixed.reconstruct(), garner.reconstruct());
        done |= mixed.reconstruct().as_ref() == Some(&truth);
    }
    assert!(done);
}

#[test]
fn mixed_radix_remaps_support() {
    let mut acc = MixedRadixAccumulator::new(3);
    assert_eq!(acc.reconstruct(), None);
    acc.add(101, &[1, 2, 3]).unwrap();
    acc.remap(&[Some(2), None, Some(0), Some(0)]);
    acc.add(103, &[3, 0, 1, 1]).unwrap();
    let int = |n: i64| BigRational::from_integer(n.into());
    assert_eq!(
        acc.reconstruct(),
        Some(vec![int(3), int(0), int(1), int(1)])
    );
    let mut empty = MixedRadixAccumulator::new(0);
    empty.add(5, &[]).unwrap();
    assert_eq!(empty.reconstruct(), Some(vec![]));
}