use std::collections::BTreeSet;
use crate::scalar::{is_prime_u128, mul_mod_u128};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Place {
Real,
Prime(u128),
}
impl std::fmt::Display for Place {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Place::Real => f.write_str("R"),
Place::Prime(p) => write!(f, "Q_{p}"),
}
}
}
fn signed_u128(sign: i128, n: u128) -> Option<i128> {
if sign < 0 {
if n == (i128::MAX as u128) + 1 {
Some(i128::MIN)
} else {
i128::try_from(n).ok()?.checked_neg()
}
} else {
i128::try_from(n).ok()
}
}
pub(crate) fn try_square_free(n: i128) -> Option<i128> {
if n == 0 {
return Some(0);
}
let sign = n.signum();
let mut n = n.unsigned_abs();
let mut res: u128 = 1;
let mut d: u128 = 2;
while d <= n / d {
if n.is_multiple_of(d) {
let mut e = 0;
while n.is_multiple_of(d) {
n /= d;
e += 1;
}
if e % 2 == 1 {
res = res.checked_mul(d)?;
}
}
d += 1;
}
if n > 1 {
res = res.checked_mul(n)?;
}
signed_u128(sign, res)
}
pub(crate) fn val_p(n: i128, p: i128) -> u128 {
let mut k = 0;
let mut n = n.unsigned_abs();
let p = p as u128;
while n.is_multiple_of(p) {
n /= p;
k += 1;
}
k
}
pub(crate) fn unit_part(mut n: i128, p: i128) -> i128 {
while n % p == 0 {
n /= p;
}
n
}
pub(crate) fn legendre(a: i128, p: i128) -> i128 {
let p_u = p as u128;
let a = a.rem_euclid(p) as u128;
if a == 0 {
return 0;
}
let mut base = a;
let mut e = (p_u - 1) / 2;
let mut acc: u128 = 1;
while e > 0 {
if e & 1 == 1 {
acc = mul_mod_u128(acc, base, p_u);
}
base = mul_mod_u128(base, base, p_u);
e >>= 1;
}
if acc == 1 {
1
} else {
-1
} }
pub fn try_is_square_qp(n: i128, p: u128) -> Option<bool> {
if !is_prime_u128(p) || i128::try_from(p).is_err() {
return None;
}
let p = p as i128;
if n == 0 {
return Some(false);
}
if !val_p(n, p).is_multiple_of(2) {
return Some(false);
}
let u = unit_part(n, p);
Some(if p == 2 {
u.rem_euclid(8) == 1
} else {
legendre(u, p) == 1
})
}
pub fn hilbert_symbol_real(a: i128, b: i128) -> i128 {
if a < 0 && b < 0 {
-1
} else {
1
}
}
fn eps2(u: i128) -> i128 {
if u.rem_euclid(4) == 1 {
0
} else {
1
}
}
fn omega2(u: i128) -> i128 {
match u.rem_euclid(8) {
1 | 7 => 0,
_ => 1, }
}
pub(crate) fn tame_hilbert_symbol(
alpha: i128,
beta: i128,
chi_a: i128,
chi_b: i128,
chi_neg1: i128,
) -> i128 {
let (a_odd, b_odd) = (alpha.rem_euclid(2) == 1, beta.rem_euclid(2) == 1);
let mut s: i128 = if a_odd && b_odd { chi_neg1 } else { 1 };
if b_odd {
s *= chi_a;
}
if a_odd {
s *= chi_b;
}
s
}
pub fn try_hilbert_symbol_qp(a: i128, b: i128, p: u128) -> Option<i128> {
if !is_prime_u128(p) || i128::try_from(p).is_err() {
return None;
}
let a = try_square_free(a)?;
let b = try_square_free(b)?;
if a == 0 || b == 0 {
return None;
}
let pi = p as i128;
let (al, be) = (val_p(a, pi), val_p(b, pi));
let (ua, ub) = (unit_part(a, pi), unit_part(b, pi));
Some(if p == 2 {
let expo = (eps2(ua) * eps2(ub) + (al as i128) * omega2(ub) + (be as i128) * omega2(ua))
.rem_euclid(2);
if expo == 0 {
1
} else {
-1
}
} else {
tame_hilbert_symbol(
al as i128,
be as i128,
legendre(ua, pi),
legendre(ub, pi),
legendre(-1, pi),
)
})
}
pub fn try_hilbert_symbol_at(a: i128, b: i128, place: Place) -> Option<i128> {
Some(match place {
Place::Real => hilbert_symbol_real(a, b),
Place::Prime(p) => try_hilbert_symbol_qp(a, b, p)?,
})
}
pub fn try_hasse_at_place(entries: &[i128], place: Place) -> Option<i128> {
let mut h = 1i128;
for i in 0..entries.len() {
for j in (i + 1)..entries.len() {
h *= try_hilbert_symbol_at(entries[i], entries[j], place)?;
}
}
Some(h)
}
pub(crate) fn try_disc_class(entries: &[i128]) -> Option<i128> {
let mut d: i128 = 1;
for &e in entries {
d = try_square_free(d.checked_mul(try_square_free(e)?)?)?;
}
Some(d)
}
fn neg_product(a: i128, b: i128) -> Option<i128> {
a.checked_mul(b)?.checked_neg()
}
pub(crate) fn try_is_isotropic_at_p(entries: &[i128], p: u128) -> Option<bool> {
let n = entries.len();
let d = try_disc_class(entries)?;
Some(match n {
0 | 1 => false,
2 => try_is_square_qp(neg_product(entries[0], entries[1])?, p)?,
3 => {
try_hilbert_symbol_qp(-1, d.checked_neg()?, p)?
== try_hasse_at_place(entries, Place::Prime(p))?
}
4 => {
!try_is_square_qp(d, p)?
|| try_hasse_at_place(entries, Place::Prime(p))?
== try_hilbert_symbol_qp(-1, -1, p)?
}
_ => true,
})
}
pub(crate) fn relevant_primes(entries: &[i128]) -> BTreeSet<u128> {
let mut ps = BTreeSet::new();
ps.insert(2);
for &e in entries {
let mut n = e.unsigned_abs();
let mut d: u128 = 2;
while d <= n / d {
if n.is_multiple_of(d) {
ps.insert(d);
while n.is_multiple_of(d) {
n /= d;
}
}
d += 1;
}
if n > 1 {
ps.insert(n);
}
}
ps
}
pub(crate) fn is_perfect_square(n: i128) -> bool {
if n < 0 {
return false;
}
let mut lo = 0i128;
let mut hi = n;
while lo <= hi {
let mid = lo + (hi - lo) / 2;
if mid == 0 || mid <= n / mid {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
hi.checked_mul(hi) == Some(n)
}
pub fn try_hilbert_reciprocity_product(a: i128, b: i128) -> Option<i128> {
let mut prod = hilbert_symbol_real(a, b);
let mut primes = relevant_primes(&[a, b]);
primes.insert(2);
for p in primes {
prod *= try_hilbert_symbol_qp(a, b, p)?;
}
Some(prod)
}
pub fn try_is_isotropic_q(entries: &[i128]) -> Option<bool> {
if entries.contains(&0) {
return Some(true); }
let n = entries.len();
if n <= 1 {
return Some(false);
}
if n == 2 {
return Some(is_perfect_square(neg_product(entries[0], entries[1])?));
}
let has_pos = entries.iter().any(|&e| e > 0);
let has_neg = entries.iter().any(|&e| e < 0);
if !(has_pos && has_neg) {
return Some(false); }
for p in relevant_primes(entries) {
if !try_is_isotropic_at_p(entries, p)? {
return Some(false);
}
}
Some(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn place_display_render_pin() {
assert_eq!(Place::Real.to_string(), "R");
assert_eq!(Place::Prime(2).to_string(), "Q_2");
assert_eq!(Place::Prime(691).to_string(), "Q_691");
}
fn sq(n: i128, p: u128) -> bool {
try_is_square_qp(n, p).expect("test prime is supported")
}
fn hs(a: i128, b: i128, p: u128) -> i128 {
try_hilbert_symbol_qp(a, b, p).expect("test Hilbert symbol is defined")
}
fn iso(entries: &[i128]) -> bool {
try_is_isotropic_q(entries).expect("test square classes fit i128")
}
#[test]
fn hilbert_symbol_is_symmetric_and_bimultiplicative_seed() {
for &p in &[2u128, 3, 5, 7] {
for a in [-3i128, -1, 1, 2, 3, 5, 6] {
for b in [-3i128, -1, 1, 2, 3, 5, 6] {
assert_eq!(hs(a, b, p), hs(b, a, p), "(a,b)_{p} symmetry");
}
}
}
for &p in &[2u128, 3, 5] {
for a in [-3i128, -1, 1, 2, 3, 5] {
assert_eq!(hs(a, -a, p), 1, "(a,−a)_{p} = 1");
}
}
}
fn reciprocity_holds(a: i128, b: i128) -> bool {
try_hilbert_reciprocity_product(a, b).expect("test symbols are defined") == 1
}
#[test]
fn hilbert_reciprocity() {
for a in -12i128..=12 {
for b in -12i128..=12 {
if a == 0 || b == 0 {
continue;
}
assert!(reciprocity_holds(a, b), "reciprocity failed at a={a} b={b}");
}
}
}
#[test]
fn hilbert_detects_nontrivial_quaternion_algebra() {
assert_eq!(hs(-1, -1, 2), -1);
assert_eq!(hilbert_symbol_real(-1, -1), -1);
for &p in &[3u128, 5, 7, 11] {
assert_eq!(hs(-1, -1, p), 1);
}
assert!(reciprocity_holds(2, 3));
}
#[test]
fn is_square_qp_basics() {
assert!(sq(2, 7));
assert!(!sq(3, 7));
assert!(!sq(7, 7));
assert!(!sq(5, 7)); assert!(sq(17, 2)); assert!(!sq(3, 2)); assert!(sq(4, 2)); }
#[test]
fn three_squares_and_sums_of_squares() {
assert!(!iso(&[1, 1, 1]));
assert!(iso(&[1, 1, -1]));
assert!(!iso(&[1, 1, 1, 1]));
assert!(iso(&[1, 1, 1, -1]));
assert!(iso(&[1, 1, 1, 1, -1]));
assert!(!iso(&[1, 1, 1, 1, 1]));
}
#[test]
fn classic_anisotropic_ternaries() {
assert!(!iso(&[1, 1, -3]));
assert!(iso(&[1, 1, -2]));
assert!(iso(&[1, 1, -5]));
assert!(iso(&[1, 1, -25])); }
#[test]
fn rank_two_is_global_square_condition() {
assert!(iso(&[1, -1])); assert!(iso(&[2, -8])); assert!(!iso(&[1, -2])); assert!(!iso(&[1, 1])); }
#[test]
fn rank_two_square_test_is_exact_near_i128_limit() {
let a = 3_037_000_499i128;
assert!(iso(&[a, -a])); assert!(!iso(&[a, -(a - 1)]));
}
#[test]
fn qp_apis_reject_nonprime_places() {
assert_eq!(try_is_square_qp(2, 9), None);
assert_eq!(try_hilbert_symbol_qp(2, 3, 1), None);
}
}