use crate::matrix::normal_form::RatUniPoly;
use rug::Rational;
use std::collections::BTreeMap;
pub const MAX_CYCLOTOMIC_ORDER: u32 = 512;
fn q_pow_minus_one(e: u32) -> RatUniPoly {
let mut coeffs = vec![Rational::from(0); e as usize + 1];
coeffs[0] = Rational::from(-1);
coeffs[e as usize] = Rational::from(1);
RatUniPoly { coeffs }
}
fn exact_div(a: &RatUniPoly, b: &RatUniPoly) -> Option<RatUniPoly> {
if b.is_zero() {
return None;
}
let (quo, rem) = RatUniPoly::div_rem(a, b);
rem.is_zero().then_some(quo)
}
pub fn cyclotomic_polynomial(d: u32) -> RatUniPoly {
debug_assert!(d >= 1, "the order of a root of unity is at least 1");
let d = d.max(1);
let mut memo: BTreeMap<u32, RatUniPoly> = BTreeMap::new();
for e in 1..=d {
if d % e != 0 {
continue;
}
let mut num = q_pow_minus_one(e);
for (&f, phi_f) in memo.iter() {
if e % f == 0 {
num = exact_div(&num, phi_f).unwrap_or(num);
}
}
memo.insert(e, num);
}
memo.remove(&d).unwrap_or_else(RatUniPoly::one)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CycloElem {
pub poly: RatUniPoly,
}
impl CycloElem {
pub fn is_zero(&self) -> bool {
self.poly.is_zero()
}
}
#[derive(Clone, Debug)]
pub struct CycloField {
d: u32,
phi: RatUniPoly,
}
impl CycloField {
pub fn new(d: u32) -> Option<CycloField> {
if d == 0 || d > MAX_CYCLOTOMIC_ORDER {
return None;
}
Some(CycloField {
d,
phi: cyclotomic_polynomial(d),
})
}
pub fn order(&self) -> u32 {
self.d
}
pub fn modulus(&self) -> &RatUniPoly {
&self.phi
}
pub fn degree(&self) -> usize {
self.phi.degree().max(0) as usize
}
pub fn reduce(&self, p: &RatUniPoly) -> CycloElem {
let (_, rem) = RatUniPoly::div_rem(p, &self.phi);
CycloElem { poly: rem.trim() }
}
pub fn zero(&self) -> CycloElem {
CycloElem {
poly: RatUniPoly::zero(),
}
}
pub fn one(&self) -> CycloElem {
self.reduce(&RatUniPoly::one())
}
pub fn from_rational(&self, c: Rational) -> CycloElem {
self.reduce(&RatUniPoly::constant(c))
}
pub fn zeta_pow(&self, e: i64) -> CycloElem {
let r = e.rem_euclid(self.d as i64) as usize;
let mut coeffs = vec![Rational::from(0); r + 1];
coeffs[r] = Rational::from(1);
self.reduce(&RatUniPoly { coeffs })
}
pub fn add(&self, a: &CycloElem, b: &CycloElem) -> CycloElem {
self.reduce(&(&a.poly + &b.poly))
}
pub fn sub(&self, a: &CycloElem, b: &CycloElem) -> CycloElem {
self.reduce(&(&a.poly - &b.poly))
}
pub fn neg(&self, a: &CycloElem) -> CycloElem {
CycloElem { poly: -&a.poly }
}
pub fn mul(&self, a: &CycloElem, b: &CycloElem) -> CycloElem {
if a.is_zero() || b.is_zero() {
return self.zero();
}
self.reduce(&(&a.poly * &b.poly))
}
pub fn inv(&self, a: &CycloElem) -> Option<CycloElem> {
if a.is_zero() {
return None;
}
let (s, _, g) = RatUniPoly::gcdex(&a.poly, &self.phi);
if g.degree() != 0 {
return None;
}
let c = g.coeffs.first()?.clone();
if c == 0 {
return None;
}
let scaled = &s * &RatUniPoly::constant(Rational::from(1) / c);
Some(self.reduce(&scaled))
}
fn poly_valuation(&self, p: &RatUniPoly) -> Option<(i64, RatUniPoly)> {
if p.is_zero() {
return None;
}
let mut v = 0_i64;
let mut cur = p.clone();
while let Some(next) = exact_div(&cur, &self.phi) {
if next.is_zero() {
break;
}
cur = next;
v += 1;
}
Some((v, cur))
}
pub fn valuation(&self, r: &crate::holonomic::qfield::Rn) -> Option<i64> {
let (vn, _) = self.poly_valuation(&r.num)?;
let (vd, _) = self
.poly_valuation(&r.den)
.unwrap_or((0, RatUniPoly::one()));
Some(vn - vd)
}
pub fn specialize(&self, r: &crate::holonomic::qfield::Rn) -> Option<CycloElem> {
let Some((vn, num)) = self.poly_valuation(&r.num) else {
return Some(self.zero());
};
let (vd, den) = self
.poly_valuation(&r.den)
.unwrap_or((0, RatUniPoly::one()));
match (vn - vd).cmp(&0) {
std::cmp::Ordering::Less => None,
std::cmp::Ordering::Greater => Some(self.zero()),
std::cmp::Ordering::Equal => {
let n = self.reduce(&num);
let d = self.reduce(&den);
let inv = self.inv(&d)?;
Some(self.mul(&n, &inv))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::qfield::{rn_one, rn_poly, Rn};
fn poly(coeffs: &[i64]) -> RatUniPoly {
RatUniPoly {
coeffs: coeffs.iter().map(|&c| Rational::from(c)).collect(),
}
.trim()
}
#[test]
fn cyclotomic_polynomials_match_the_classical_table() {
assert_eq!(cyclotomic_polynomial(1), poly(&[-1, 1])); assert_eq!(cyclotomic_polynomial(2), poly(&[1, 1])); assert_eq!(cyclotomic_polynomial(3), poly(&[1, 1, 1]));
assert_eq!(cyclotomic_polynomial(4), poly(&[1, 0, 1]));
assert_eq!(cyclotomic_polynomial(5), poly(&[1, 1, 1, 1, 1]));
assert_eq!(cyclotomic_polynomial(6), poly(&[1, -1, 1]));
assert_eq!(cyclotomic_polynomial(12), poly(&[1, 0, -1, 0, 1]));
assert_eq!(cyclotomic_polynomial(105).coeffs[7], Rational::from(-2));
}
#[test]
fn the_product_over_divisors_is_q_to_the_d_minus_one() {
for d in 1_u32..=24 {
let mut prod = RatUniPoly::one();
for e in 1..=d {
if d % e == 0 {
prod = &prod * &cyclotomic_polynomial(e);
}
}
assert_eq!(
prod,
q_pow_minus_one(d),
"prod_(e|{d}) Phi_e must be q^{d} − 1"
);
}
}
#[test]
fn degree_is_eulers_totient() {
let totient = |d: u32| (1..=d).filter(|&e| gcd(e, d) == 1).count();
for d in 1_u32..=40 {
let f = CycloField::new(d).expect("in range");
assert_eq!(f.degree(), totient(d), "deg Phi_{d} must be phi({d})");
}
}
fn gcd(a: u32, b: u32) -> u32 {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
#[test]
fn zeta_has_order_exactly_d() {
for d in 1_u32..=12 {
let f = CycloField::new(d).expect("in range");
assert_eq!(f.zeta_pow(d as i64), f.one(), "zeta^{d} must be 1");
assert_eq!(f.zeta_pow(-1), f.zeta_pow(d as i64 - 1));
for e in 1..d {
assert_ne!(f.zeta_pow(e as i64), f.one(), "zeta^{e} must not be 1");
}
}
}
#[test]
fn inverse_is_a_two_sided_inverse() {
for d in [1_u32, 2, 3, 4, 5, 6, 8, 12] {
let f = CycloField::new(d).expect("in range");
for e in 0..d as i64 {
let a = f.add(&f.zeta_pow(e), &f.from_rational(Rational::from(3)));
if a.is_zero() {
continue;
}
let inv = f.inv(&a).expect("nonzero elements are invertible");
assert_eq!(f.mul(&a, &inv), f.one());
}
}
}
#[test]
fn valuation_counts_the_phi_factors_exactly() {
let f = CycloField::new(3).expect("in range");
let r: Rn = rn_poly(q_pow_minus_one(3));
assert_eq!(f.valuation(&r), Some(1));
let phi2 = &cyclotomic_polynomial(3) * &cyclotomic_polynomial(3);
assert_eq!(f.valuation(&rn_poly(phi2)), Some(2));
let inv = crate::holonomic::qfield::rn_inv(&r).expect("nonzero");
assert_eq!(f.valuation(&inv), Some(-1));
assert!(f.specialize(&inv).is_none(), "a pole must not specialise");
assert_eq!(f.valuation(&crate::holonomic::qfield::rn_zero()), None);
assert_eq!(f.valuation(&rn_one()), Some(0));
}
#[test]
fn a_zero_over_zero_representation_still_specialises() {
let f = CycloField::new(3).expect("in range");
let cube = q_pow_minus_one(3);
let r = Rn {
num: cube.clone(),
den: cube,
};
assert_eq!(f.specialize(&r), Some(f.one()));
assert_eq!(f.valuation(&r), Some(0));
}
#[test]
fn specialising_a_root_of_unity_power_agrees_with_zeta_pow() {
for d in 1_u32..=8 {
let f = CycloField::new(d).expect("in range");
for e in 0..12_i64 {
let r = super::super::field::qq_pow(e);
assert_eq!(f.specialize(&r), Some(f.zeta_pow(e)));
}
}
}
#[test]
fn out_of_range_orders_are_refused() {
assert!(CycloField::new(0).is_none());
assert!(CycloField::new(MAX_CYCLOTOMIC_ORDER + 1).is_none());
assert!(CycloField::new(MAX_CYCLOTOMIC_ORDER).is_some());
}
}