use num::BigInt;
use crate::multiword_int as mw;
use crate::polynomial::Polynomial;
pub(crate) const SCHOOLBOOK_MAX_N: usize = 64;
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct MultiwordPoly {
n: usize,
w: usize,
data: Vec<u64>,
}
impl MultiwordPoly {
pub(crate) fn zeros(n: usize, w: usize) -> Self {
Self {
n,
w,
data: vec![0u64; n * w],
}
}
pub(crate) fn n(&self) -> usize {
self.n
}
pub(crate) fn coeff(&self, i: usize) -> &[u64] {
&self.data[i * self.w..(i + 1) * self.w]
}
pub(crate) fn coeff_mut(&mut self, i: usize) -> &mut [u64] {
&mut self.data[i * self.w..(i + 1) * self.w]
}
pub(crate) fn from_bigint_poly(p: &Polynomial<BigInt>, w: usize) -> Self {
let n = p.coefficients.len();
let mut out = Self::zeros(n, w);
for (i, c) in p.coefficients.iter().enumerate() {
mw::from_bigint_into(out.coeff_mut(i), c);
}
out
}
pub(crate) fn to_bigint_poly(&self) -> Polynomial<BigInt> {
Polynomial::new((0..self.n).map(|i| mw::to_bigint(self.coeff(i))).collect())
}
pub(crate) fn shl_coeffs(&self, bits: u32) -> Self {
let mut out = Self::zeros(self.n, self.w);
for i in 0..self.n {
mw::shl_into(out.coeff_mut(i), self.coeff(i), bits);
}
out
}
pub(crate) fn sub(&self, other: &Self) -> Self {
debug_assert!(self.n == other.n && self.w == other.w);
let mut out = Self::zeros(self.n, self.w);
for i in 0..self.n {
mw::sub_into(out.coeff_mut(i), self.coeff(i), other.coeff(i));
}
out
}
pub(crate) fn sub_assign(&mut self, other: &Self) {
debug_assert!(self.n == other.n && self.w == other.w);
for i in 0..self.n {
let (dst, src) = (
&mut self.data[i * self.w..(i + 1) * self.w],
&other.data[i * self.w..(i + 1) * self.w],
);
mw::sub_into_self(dst, src);
}
}
pub(crate) fn max_coeff_bits(&self) -> u64 {
(0..self.n)
.map(|i| mw::bit_length(self.coeff(i)))
.max()
.unwrap_or(0)
}
fn magnitudes(&self) -> (Self, Vec<bool>) {
let mut mag = self.clone();
let mut signs = vec![false; self.n];
for (i, signs_i) in signs.iter_mut().enumerate().take(self.n) {
let c = mag.coeff_mut(i);
if mw::is_negative(c) {
mw::neg_in_place(c);
*signs_i = true;
}
}
(mag, signs)
}
pub(crate) fn schoolbook_negacyclic_mul(&self, other: &Self, out_w: usize) -> Self {
debug_assert_eq!(self.n, other.n);
let n = self.n;
let (amag, asign) = self.magnitudes();
let (bmag, bsign) = other.magnitudes();
let mut out = Self::zeros(n, out_w);
let mut prod = vec![0u64; out_w];
for (i, &asign_i) in asign.iter().enumerate().take(n) {
for (j, &bsign_j) in bsign.iter().enumerate().take(n) {
mw::umul_into(&mut prod, amag.coeff(i), bmag.coeff(j));
let k = i + j;
let (slot, wrap) = if k < n { (k, false) } else { (k - n, true) };
if asign_i ^ bsign_j ^ wrap {
mw::sub_into_self(out.coeff_mut(slot), &prod);
} else {
mw::add_into_self(out.coeff_mut(slot), &prod);
}
}
}
out
}
pub(crate) fn negacyclic_mul(&self, other: &Self, out_w: usize) -> Self {
if self.n <= SCHOOLBOOK_MAX_N {
self.schoolbook_negacyclic_mul(other, out_w)
} else {
let prod_bits =
self.max_coeff_bits() + other.max_coeff_bits() + self.n.ilog2() as u64 + 2;
let k = (prod_bits as usize) / 23 + 2;
let ctx = crate::rns_runtime::RuntimeNtt::cached(self.n, k);
ctx.negacyclic_mul(self, other, out_w)
}
}
pub(crate) fn field_norm(&self) -> Self {
let n = self.n;
debug_assert!(n >= 2 && n.is_multiple_of(2));
let half = n / 2;
let mut f0 = Self::zeros(half, self.w);
let mut f1 = Self::zeros(half, self.w);
for i in 0..half {
f0.coeff_mut(i).copy_from_slice(self.coeff(2 * i));
f1.coeff_mut(i).copy_from_slice(self.coeff(2 * i + 1));
}
let b = f0.max_coeff_bits().max(f1.max_coeff_bits());
let prod_bits = 2 * b + (half.max(1)).ilog2() as u64 + 2;
let out_w = (prod_bits / 64 + 1) as usize;
let f0_sq = f0.negacyclic_mul(&f0, out_w);
let f1_sq = f1.negacyclic_mul(&f1, out_w);
let mut out = f0_sq;
mw::add_into_self(out.coeff_mut(0), f1_sq.coeff(half - 1));
for i in 1..half {
let (dst, src) = (
&mut out.data[i * out_w..(i + 1) * out_w],
f1_sq.coeff(i - 1),
);
mw::sub_into_self(dst, src);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::polynomial::Polynomial;
use num::BigInt;
use rand::{rngs::StdRng, RngExt, SeedableRng};
const W: usize = 6;
impl MultiwordPoly {
pub(crate) fn lift_next_cyclotomic(&self) -> Self {
let mut out = Self::zeros(self.n * 2, self.w);
for i in 0..self.n {
out.coeff_mut(2 * i).copy_from_slice(self.coeff(i));
}
out
}
pub(crate) fn galois_adjoint(&self) -> Self {
let mut out = self.clone();
for i in (1..self.n).step_by(2) {
mw::neg_in_place(out.coeff_mut(i));
}
out
}
pub(crate) fn reduce_by_cyclotomic(&self, n_target: usize) -> Self {
let mut out = Self::zeros(n_target, self.w);
for i in 0..self.n {
let slot = i % n_target;
let src = self.coeff(i);
let dst = out.coeff_mut(slot);
if (i / n_target).is_multiple_of(2) {
mw::add_into_self(dst, src);
} else {
mw::sub_into_self(dst, src);
}
}
out
}
}
fn rand_bigint(rng: &mut StdRng) -> BigInt {
let hi = BigInt::from(rng.random::<i128>());
let lo = BigInt::from(rng.random::<u64>());
(hi << 64) + lo
}
fn rand_poly(rng: &mut StdRng, n: usize) -> Polynomial<BigInt> {
Polynomial::new((0..n).map(|_| rand_bigint(rng)).collect())
}
fn same(mw: &MultiwordPoly, bi: &Polynomial<BigInt>) {
assert_eq!(mw.to_bigint_poly().coefficients, bi.coefficients);
}
#[test]
fn roundtrip_bigint_poly() {
let mut rng = StdRng::seed_from_u64(10);
for _ in 0..200 {
let p = rand_poly(&mut rng, 16);
let m = MultiwordPoly::from_bigint_poly(&p, W);
same(&m, &p);
}
}
#[test]
fn lift_matches_bigint() {
let mut rng = StdRng::seed_from_u64(11);
for &n in &[2usize, 4, 8, 16] {
let p = rand_poly(&mut rng, n);
let m = MultiwordPoly::from_bigint_poly(&p, W);
same(&m.lift_next_cyclotomic(), &p.lift_next_cyclotomic());
}
}
#[test]
fn galois_matches_bigint() {
let mut rng = StdRng::seed_from_u64(12);
for &n in &[2usize, 4, 8, 16] {
let p = rand_poly(&mut rng, n);
let m = MultiwordPoly::from_bigint_poly(&p, W);
same(&m.galois_adjoint(), &p.galois_adjoint());
}
}
#[test]
fn sub_matches_bigint() {
let mut rng = StdRng::seed_from_u64(13);
for _ in 0..200 {
let a = rand_poly(&mut rng, 16);
let b = rand_poly(&mut rng, 16);
let ma = MultiwordPoly::from_bigint_poly(&a, W);
let mb = MultiwordPoly::from_bigint_poly(&b, W);
same(&ma.sub(&mb), &(a.clone() - b.clone()));
let mut acc = ma.clone();
acc.sub_assign(&mb);
same(&acc, &(a - b));
}
}
#[test]
fn shl_coeffs_matches_bigint() {
let mut rng = StdRng::seed_from_u64(14);
for _ in 0..100 {
let p = Polynomial::new((0..16).map(|_| BigInt::from(rng.random::<i64>())).collect());
let m = MultiwordPoly::from_bigint_poly(&p, W);
for &s in &[0u32, 1, 13, 64, 130] {
let want = Polynomial::new(p.coefficients.iter().map(|c| c << s).collect());
same(&m.shl_coeffs(s), &want);
}
}
}
fn negacyclic_bigint(a: &Polynomial<BigInt>, b: &Polynomial<BigInt>) -> Polynomial<BigInt> {
let n = a.coefficients.len();
let mut out = vec![BigInt::from(0); n];
for i in 0..n {
for j in 0..n {
let prod = &a.coefficients[i] * &b.coefficients[j];
let k = i + j;
if k < n {
out[k] += ∏
} else {
out[k - n] -= ∏
}
}
}
Polynomial::new(out)
}
#[test]
fn schoolbook_negacyclic_matches_bigint() {
let mut rng = StdRng::seed_from_u64(20);
const OUT_W: usize = 12;
for &n in &[1usize, 2, 4, 8] {
for _ in 0..50 {
let a = rand_poly(&mut rng, n);
let b = rand_poly(&mut rng, n);
let ma = MultiwordPoly::from_bigint_poly(&a, W);
let mb = MultiwordPoly::from_bigint_poly(&b, W);
let got = ma.schoolbook_negacyclic_mul(&mb, OUT_W);
same(&got, &negacyclic_bigint(&a, &b));
}
}
}
#[test]
fn negacyclic_mul_dispatch_matches_bigint() {
let mut rng = StdRng::seed_from_u64(21);
const OUT_W: usize = 12;
for &n in &[4usize, 8, 16, 32, 64] {
for _ in 0..10 {
let a = rand_poly(&mut rng, n);
let b = rand_poly(&mut rng, n);
let ma = MultiwordPoly::from_bigint_poly(&a, W);
let mb = MultiwordPoly::from_bigint_poly(&b, W);
same(&ma.negacyclic_mul(&mb, OUT_W), &negacyclic_bigint(&a, &b));
}
}
}
#[test]
fn field_norm_matches_bigint() {
let mut rng = StdRng::seed_from_u64(22);
for &n in &[2usize, 4, 8, 16, 32, 64] {
for _ in 0..10 {
let p = rand_poly(&mut rng, n);
let mp = MultiwordPoly::from_bigint_poly(&p, W);
same(&mp.field_norm(), &p.field_norm());
}
}
}
#[test]
fn reduce_by_cyclotomic_matches_bigint() {
let mut rng = StdRng::seed_from_u64(15);
for &n_target in &[2usize, 4, 8] {
for &m_len in &[n_target, 2 * n_target - 1, 2 * n_target] {
let p = rand_poly(&mut rng, m_len);
let mp = MultiwordPoly::from_bigint_poly(&p, W);
same(
&mp.reduce_by_cyclotomic(n_target),
&p.reduce_by_cyclotomic(n_target),
);
}
}
}
}