use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::Int;
use crate::nat::Nat;
#[inline]
fn int_from_i64(x: i64) -> Int {
if x >= 0 {
Int::from_u64(x as u64)
} else {
Int::from_u64(x.unsigned_abs()).neg()
}
}
fn mod_u64(n: &Nat, p: u64) -> u64 {
let mut r: u128 = 0;
for &limb in n.as_limbs().iter().rev() {
r = ((r << 64) | limb as u128) % p as u128;
}
r as u64
}
fn sqrt_mod_u64(a: u64, p: u64) -> Option<u64> {
let a = a % p;
if a == 0 {
return Some(0);
}
if p == 2 {
return Some(a & 1);
}
if pow_mod_u64(a, (p - 1) / 2, p) != 1 {
return None; }
if p % 4 == 3 {
return Some(pow_mod_u64(a, (p + 1) / 4, p));
}
let mut q = p - 1;
let mut s = 0u32;
while q & 1 == 0 {
q >>= 1;
s += 1;
}
let mut z = 2u64;
while pow_mod_u64(z, (p - 1) / 2, p) != p - 1 {
z += 1;
}
let mut m = s;
let mut c = pow_mod_u64(z, q, p);
let mut t = pow_mod_u64(a, q, p);
let mut r = pow_mod_u64(a, q.div_ceil(2), p);
while t != 1 {
let mut i = 0u32;
let mut t2 = t;
while t2 != 1 {
t2 = mul_mod_u64(t2, t2, p);
i += 1;
}
let b = pow_mod_u64(c, 1u64 << (m - i - 1), p);
m = i;
c = mul_mod_u64(b, b, p);
t = mul_mod_u64(t, c, p);
r = mul_mod_u64(r, b, p);
}
Some(r)
}
#[inline]
fn mul_mod_u64(a: u64, b: u64, p: u64) -> u64 {
((a as u128 * b as u128) % p as u128) as u64
}
fn pow_mod_u64(mut base: u64, mut exp: u64, p: u64) -> u64 {
let mut r = 1u64;
base %= p;
while exp > 0 {
if exp & 1 == 1 {
r = mul_mod_u64(r, base, p);
}
base = mul_mod_u64(base, base, p);
exp >>= 1;
}
r
}
fn primes_up_to(limit: u64) -> Vec<u64> {
if limit < 2 {
return Vec::new();
}
let n = limit as usize + 1;
let mut sieve = alloc::vec![true; n];
sieve[0] = false;
sieve[1] = false;
let mut i = 2usize;
while i * i < n {
if sieve[i] {
let mut j = i * i;
while j < n {
sieve[j] = false;
j += i;
}
}
i += 1;
}
(2..n).filter(|&i| sieve[i]).map(|i| i as u64).collect()
}
struct FbPrime {
p: u64,
logp: u8,
root1: u64,
root2: u64,
}
struct Relation {
base: Nat,
exps: Vec<(usize, u32)>,
parity: Vec<u64>,
}
struct Params {
bound: u64,
m: u64,
fudge: u32,
}
fn params_for(digits: usize) -> Params {
let (bound, m): (u64, u64) = match digits {
0..=24 => (3_000, 300_000),
25..=29 => (10_000, 4_000_000),
30..=34 => (25_000, 10_000_000),
35..=39 => (45_000, 20_000_000),
40..=42 => (75_000, 40_000_000),
43..=45 => (120_000, 70_000_000),
_ => (250_000, 120_000_000),
};
let fudge = (64 - bound.leading_zeros()) + 2;
Params { bound, m, fudge }
}
fn build_factor_base(n: &Nat, bound: u64) -> Vec<FbPrime> {
let mut fb = Vec::new();
for p in primes_up_to(bound) {
let a = mod_u64(n, p);
if p == 2 {
fb.push(FbPrime {
p: 2,
logp: 1,
root1: 1,
root2: 1,
});
continue;
}
if a == 0 {
fb.push(FbPrime {
p,
logp: 64 - p.leading_zeros() as u8 - 1,
root1: 0,
root2: p,
});
continue;
}
if let Some(r) = sqrt_mod_u64(a, p) {
fb.push(FbPrime {
p,
logp: (64 - p.leading_zeros() - 1) as u8,
root1: r,
root2: p - r,
});
}
}
fb
}
fn collect_relations(
n: &Nat,
a: &Nat,
fb: &[FbPrime],
params: &Params,
m: u64,
want: usize,
) -> Vec<Relation> {
let width = (2 * m + 1) as usize;
let mut logs = alloc::vec![0u8; width];
for fp in fb {
if fp.p == 2 {
let a_par = mod_u64(a, 2) as i64;
let mut i = (1 - a_par - m as i64).rem_euclid(2) as usize;
while i < width {
logs[i] = logs[i].saturating_add(1);
i += 2;
}
continue;
}
let a_mod = mod_u64(a, fp.p) as i64;
let single = fp.root1 == fp.root2;
for (k, &root) in [fp.root1, fp.root2].iter().enumerate() {
if k == 1 && single {
break; }
let start = (root as i64 - a_mod + m as i64).rem_euclid(fp.p as i64) as usize;
let mut i = start;
while i < width {
logs[i] = logs[i].saturating_add(fp.logp);
i += fp.p as usize;
}
}
}
let half_log2n = (n.bit_len() / 2) as u32;
let mut relations = Vec::new();
#[allow(clippy::needless_range_loop)] for i in 0..width {
let x = i as i64 - m as i64;
let xlog = if x == 0 {
0
} else {
63 - (x.unsigned_abs()).leading_zeros()
};
let target = half_log2n.saturating_add(xlog);
if (logs[i] as u32) + params.fudge < target {
continue;
}
if let Some(rel) = try_relation(n, a, fb, x) {
relations.push(rel);
if relations.len() >= want {
break;
}
}
}
relations
}
fn try_relation(n: &Nat, a: &Nat, fb: &[FbPrime], x: i64) -> Option<Relation> {
let base = if x >= 0 {
a.add(&Nat::from_u64(x as u64))
} else {
a.checked_sub(&Nat::from_u64((-x) as u64))?
};
relation_from_base(n, base, fb)
}
fn relation_from_base(n: &Nat, base: Nat, fb: &[FbPrime]) -> Option<Relation> {
let sq = base.square();
let (mut mag, neg) = if sq >= *n {
(sq.checked_sub(n).unwrap(), false)
} else {
(n.checked_sub(&sq).unwrap(), true)
};
if mag.is_zero() {
return None;
}
let mut exps: Vec<(usize, u32)> = Vec::new();
if neg {
exps.push((0, 1)); }
for (idx, fp) in fb.iter().enumerate().skip(1) {
let pn = Nat::from_u64(fp.p);
let mut e = 0u32;
loop {
let (q, r) = mag.div_rem(&pn).unwrap();
if !r.is_zero() {
break;
}
mag = q;
e += 1;
}
if e > 0 {
exps.push((idx, e));
}
if mag.is_one() {
break;
}
}
if !mag.is_one() {
return None; }
let words = fb.len().div_ceil(64);
let mut parity = alloc::vec![0u64; words];
for &(idx, e) in &exps {
if e & 1 == 1 {
parity[idx / 64] ^= 1u64 << (idx % 64);
}
}
Some(Relation { base, exps, parity })
}
fn find_dependencies(relations: &[Relation], cols: usize) -> Vec<Vec<usize>> {
let m = relations.len();
let pword = cols.div_ceil(64);
let hword = m.div_ceil(64);
let mut par: Vec<Vec<u64>> = relations.iter().map(|r| r.parity.clone()).collect();
for row in &mut par {
row.resize(pword, 0);
}
let mut hist: Vec<Vec<u64>> = (0..m)
.map(|i| {
let mut h = alloc::vec![0u64; hword];
h[i / 64] |= 1u64 << (i % 64);
h
})
.collect();
let mut pivot_row: Vec<Option<usize>> = alloc::vec![None; cols];
let mut deps = Vec::new();
for i in 0..m {
for c in 0..cols {
if par[i][c / 64] & (1u64 << (c % 64)) == 0 {
continue;
}
if let Some(pr) = pivot_row[c] {
let (lo, hi) = par.split_at_mut(i);
for (a, b) in hi[0].iter_mut().zip(&lo[pr]) {
*a ^= *b;
}
let (lo, hi) = hist.split_at_mut(i);
for (a, b) in hi[0].iter_mut().zip(&lo[pr]) {
*a ^= *b;
}
}
}
let mut pivot = None;
for c in 0..cols {
if par[i][c / 64] & (1u64 << (c % 64)) != 0 {
pivot = Some(c);
break;
}
}
match pivot {
Some(c) => pivot_row[c] = Some(i),
None => {
let subset: Vec<usize> = (0..m)
.filter(|&j| hist[i][j / 64] & (1u64 << (j % 64)) != 0)
.collect();
if !subset.is_empty() {
deps.push(subset);
}
}
}
}
deps
}
fn factor_from_dependencies(
n: &Nat,
fb: &[FbPrime],
relations: &[Relation],
deps: &[Vec<usize>],
) -> Option<Nat> {
for subset in deps {
let mut x = Nat::one();
let mut total: Vec<u32> = alloc::vec![0u32; fb.len()];
for &i in subset {
let r = &relations[i];
x = r.base.mul(&x).div_rem(n).unwrap().1;
for &(idx, e) in &r.exps {
total[idx] += e;
}
}
let mut y = Nat::one();
for (idx, &e) in total.iter().enumerate().skip(1) {
if e == 0 {
continue;
}
debug_assert_eq!(e & 1, 0, "dependency exponent must be even");
let pe = Nat::from_u64(fb[idx].p).modpow(&Nat::from_u64((e / 2) as u64), n);
y = y.mul(&pe).div_rem(n).unwrap().1;
}
let diff = if x >= y {
x.checked_sub(&y).unwrap()
} else {
n.checked_sub(&y.checked_sub(&x).unwrap()).unwrap()
};
if !diff.is_zero() {
let g = diff.gcd(n);
if !g.is_one() && &g != n {
return Some(g);
}
}
let sum = x.add(&y).div_rem(n).unwrap().1;
if !sum.is_zero() {
let g = sum.gcd(n);
if !g.is_one() && &g != n {
return Some(g);
}
}
}
None
}
pub(crate) fn qs_factor(n: &Nat) -> Option<Nat> {
let digits = (n.bit_len() * 30103 / 100000 + 1) as usize;
let params = params_for(digits);
let fb = build_factor_base(n, params.bound);
if fb.len() < 3 {
return None;
}
let root = n.isqrt();
let a = if root.square() == *n {
return Some(root); } else {
root.add(&Nat::one())
};
let want = fb.len() + 16 + fb.len() / 20;
const M_CAP: u64 = 300_000_000;
let mut m = params.m.min(M_CAP);
let mut relations = collect_relations(n, &a, &fb, ¶ms, m, want);
while relations.len() < want && m < M_CAP {
m = m.saturating_mul(2).min(M_CAP);
relations = collect_relations(n, &a, &fb, ¶ms, m, want);
}
if relations.len() <= fb.len() {
return None; }
let deps = find_dependencies(&relations, fb.len());
factor_from_dependencies(n, &fb, &relations, &deps)
}
#[inline]
fn inv_mod_u64(a: u64, p: u64) -> u64 {
if p == 2 {
return 1; }
pow_mod_u64(a % p, p - 2, p)
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn below(&mut self, bound: usize) -> usize {
if bound == 0 {
0
} else {
(self.next() >> 33) as usize % bound
}
}
}
struct SiqsParams {
bound: u64,
m: u64,
s: usize,
}
fn siqs_params_for(digits: usize) -> SiqsParams {
let (bound, m, s): (u64, u64, usize) = match digits {
0..=32 => (3_000, 60_000, 3),
33..=37 => (7_000, 65_000, 4),
38..=42 => (18_000, 65_000, 5),
43..=46 => (40_000, 100_000, 6),
47..=50 => (80_000, 120_000, 7),
_ => (150_000, 160_000, 8),
};
SiqsParams { bound, m, s }
}
struct ACoeff {
a: Nat,
b_terms: Vec<Nat>,
active: Vec<bool>,
soln1: Vec<u64>,
soln2: Vec<u64>,
bainv2: Vec<u64>,
}
fn choose_a(
n: &Nat,
fb: &[FbPrime],
params: &SiqsParams,
rng: &mut Lcg,
) -> Option<(Nat, Vec<usize>)> {
let two_n = n.add(n);
let target = two_n.isqrt().div_rem(&Nat::from_u64(params.m)).unwrap().0;
if target.bit_len() < 10 {
return None;
}
let per_prime = target.nth_root_floor(params.s as u32).to_u64().unwrap_or(0);
if per_prime < 3 {
return None;
}
let mut pool: Vec<usize> = (1..fb.len())
.filter(|&i| fb[i].p > 2 && fb[i].root1 != 0)
.collect();
pool.sort_by_key(|&i| fb[i].p.abs_diff(per_prime));
let pool_len = pool.len().min((params.s * 6).max(30));
pool.truncate(pool_len);
if pool.len() < params.s {
return None;
}
let mut chosen: Vec<usize> = Vec::with_capacity(params.s);
let mut prod = Nat::one();
while chosen.len() + 1 < params.s {
let cand = pool[rng.below(pool.len())];
if chosen.contains(&cand) {
continue;
}
chosen.push(cand);
prod = prod.mul(&Nat::from_u64(fb[cand].p));
}
let want_last = target
.div_rem(&prod)
.unwrap()
.0
.to_u64()
.unwrap_or(u64::MAX);
let mut best: Option<usize> = None;
for &i in &pool {
if chosen.contains(&i) {
continue;
}
let better = match best {
None => true,
Some(b) => fb[i].p.abs_diff(want_last) < fb[b].p.abs_diff(want_last),
};
if better {
best = Some(i);
}
}
let last = best?;
chosen.push(last);
prod = prod.mul(&Nat::from_u64(fb[last].p));
chosen.sort_unstable();
Some((prod, chosen))
}
fn init_a(fb: &[FbPrime], m: u64, a: Nat, a_idx: Vec<usize>) -> ACoeff {
let s = a_idx.len();
let mut b_terms: Vec<Nat> = Vec::with_capacity(s);
for &k in &a_idx {
let q = fb[k].p;
let a_over_q = a.div_rem(&Nat::from_u64(q)).unwrap().0;
let inv = inv_mod_u64(mod_u64(&a_over_q, q), q);
let gamma = mul_mod_u64(fb[k].root1, inv, q);
let gamma = gamma.min(q - gamma);
b_terms.push(a_over_q.mul(&Nat::from_u64(gamma)));
}
let mut b = Nat::zero();
for t in &b_terms {
b = b.add(t);
}
let mut active = alloc::vec![false; fb.len()];
let mut soln1 = alloc::vec![0u64; fb.len()];
let mut soln2 = alloc::vec![0u64; fb.len()];
let mut bainv2 = alloc::vec![0u64; fb.len() * s];
let mmod = m;
for (idx, fp) in fb.iter().enumerate().skip(1) {
let p = fp.p;
if a.div_rem(&Nat::from_u64(p)).unwrap().1.is_zero() {
continue; }
active[idx] = true;
let ai = inv_mod_u64(mod_u64(&a, p), p);
for (k, term) in b_terms.iter().enumerate() {
bainv2[idx * s + k] = mul_mod_u64(mul_mod_u64(2 % p, mod_u64(term, p), p), ai, p);
}
let t = fp.root1;
let bmod = mod_u64(&b, p) as i128;
let r1 = (ai as i128 * ((t as i128) - bmod)).rem_euclid(p as i128) as u64;
let r2 = (ai as i128 * ((p as i128 - t as i128) - bmod)).rem_euclid(p as i128) as u64;
soln1[idx] = ((r1 as i128 + mmod as i128) % p as i128) as u64;
soln2[idx] = ((r2 as i128 + mmod as i128) % p as i128) as u64;
}
ACoeff {
a,
b_terms,
active,
soln1,
soln2,
bainv2,
}
}
struct LpStore {
reps: BTreeMap<u64, (Nat, Vec<(usize, u32)>)>,
#[cfg_attr(not(test), allow(dead_code))]
direct: usize,
#[cfg_attr(not(test), allow(dead_code))]
combined: usize,
#[cfg_attr(not(test), allow(dead_code))]
partials_seen: usize,
}
impl LpStore {
fn new() -> Self {
LpStore {
reps: BTreeMap::new(),
direct: 0,
combined: 0,
partials_seen: 0,
}
}
}
#[allow(clippy::too_many_arguments)]
fn sieve_poly(
n: &Nat,
ac: &ACoeff,
b: &Int,
fb: &[FbPrime],
m: u64,
target: u32,
fudge: u32,
lp_bound: u64,
logs: &mut [u8],
relations: &mut Vec<Relation>,
lp: &mut LpStore,
want: usize,
) {
let width = (2 * m + 1) as usize;
for v in logs.iter_mut() {
*v = 0;
}
for (idx, fp) in fb.iter().enumerate().skip(1) {
if !ac.active[idx] {
continue;
}
let p = fp.p as usize;
let single = fp.root1 == fp.root2;
for (k, &root) in [ac.soln1[idx], ac.soln2[idx]].iter().enumerate() {
if k == 1 && single {
break;
}
let mut i = root as usize;
while i < width {
logs[i] = logs[i].saturating_add(fp.logp);
i += p;
}
}
}
let a_int = Int::from(ac.a.clone());
let threshold = target.saturating_sub(fudge);
#[allow(clippy::needless_range_loop)] for i in 0..width {
if (logs[i] as u32) < threshold {
continue;
}
let x = i as i64 - m as i64;
let g = a_int.mul(&int_from_i64(x)).add(b);
let base = g.magnitude();
if base.is_zero() {
continue;
}
match siqs_relation(n, ac, base, fb, i, lp_bound) {
Cand::Full(rel) => {
lp.direct += 1;
relations.push(rel);
}
Cand::Partial {
lp: prime,
base,
exps,
} => {
lp.partials_seen += 1;
match lp.reps.get(&prime) {
Some((b0, e0)) => {
if let Some(rel) =
combine_partials(n, prime, b0, e0, &base, &exps, fb.len())
{
lp.combined += 1;
relations.push(rel);
}
}
None => {
lp.reps.insert(prime, (base, exps));
}
}
}
Cand::None => {}
}
if relations.len() >= want {
return;
}
}
}
enum Cand {
Full(Relation),
Partial {
lp: u64,
base: Nat,
exps: Vec<(usize, u32)>,
},
None,
}
fn parity_of(exps: &[(usize, u32)], cols: usize) -> Vec<u64> {
let mut parity = alloc::vec![0u64; cols.div_ceil(64)];
for &(idx, e) in exps {
if e & 1 == 1 {
parity[idx / 64] ^= 1u64 << (idx % 64);
}
}
parity
}
fn siqs_relation(n: &Nat, ac: &ACoeff, base: Nat, fb: &[FbPrime], i: usize, lp_bound: u64) -> Cand {
let sq = base.square();
let (mut mag, neg) = if sq >= *n {
(sq.checked_sub(n).unwrap(), false)
} else {
(n.checked_sub(&sq).unwrap(), true)
};
if mag.is_zero() {
return Cand::None;
}
let mut exps: Vec<(usize, u32)> = Vec::new();
if neg {
exps.push((0, 1));
}
for (idx, fp) in fb.iter().enumerate().skip(1) {
let p = fp.p;
let hits = if ac.active[idx] {
let r = (i as u64) % p;
r == ac.soln1[idx] || r == ac.soln2[idx]
} else {
true
};
if !hits {
continue;
}
let pn = Nat::from_u64(p);
let mut e = 0u32;
loop {
let (q, r) = mag.div_rem(&pn).unwrap();
if !r.is_zero() {
break;
}
mag = q;
e += 1;
}
if e > 0 {
exps.push((idx, e));
}
if mag.is_one() {
break;
}
}
if mag.is_one() {
let parity = parity_of(&exps, fb.len());
return Cand::Full(Relation { base, exps, parity });
}
if lp_bound > 0
&& mag.bit_len() <= 64
&& let Some(lp) = mag.to_u64()
&& lp <= lp_bound
&& mag.is_prime_bpsw()
{
return Cand::Partial { lp, base, exps };
}
Cand::None }
fn merge_exps(a: &[(usize, u32)], b: &[(usize, u32)]) -> Vec<(usize, u32)> {
let mut out: Vec<(usize, u32)> = Vec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() || j < b.len() {
match (a.get(i), b.get(j)) {
(Some(&(ia, ea)), Some(&(ib, eb))) => {
if ia < ib {
out.push((ia, ea));
i += 1;
} else if ib < ia {
out.push((ib, eb));
j += 1;
} else {
out.push((ia, ea + eb));
i += 1;
j += 1;
}
}
(Some(&e), None) => {
out.push(e);
i += 1;
}
(None, Some(&e)) => {
out.push(e);
j += 1;
}
(None, None) => break,
}
}
out
}
fn combine_partials(
n: &Nat,
lp: u64,
b1: &Nat,
e1: &[(usize, u32)],
b2: &Nat,
e2: &[(usize, u32)],
cols: usize,
) -> Option<Relation> {
let inv = Int::from_u64(lp).modinv(&Int::from(n.clone()))?.magnitude();
let base = b1
.mul(b2)
.div_rem(n)
.unwrap()
.1
.mul(&inv)
.div_rem(n)
.unwrap()
.1;
if base.is_zero() {
return None;
}
let exps = merge_exps(e1, e2);
let parity = parity_of(&exps, cols);
Some(Relation { base, exps, parity })
}
pub(crate) fn siqs_factor(n: &Nat) -> Option<Nat> {
siqs_run(n, LargePrime::Single).0
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LargePrime {
#[cfg_attr(not(test), allow(dead_code))]
Off,
Single,
}
fn siqs_run(n: &Nat, policy: LargePrime) -> (Option<Nat>, LpStore) {
let digits = (n.bit_len() * 30103 / 100000 + 1) as usize;
let params = siqs_params_for(digits);
let mut lp = LpStore::new();
let fb = build_factor_base(n, params.bound);
if fb.len() < 10 {
return (None, lp);
}
let root = n.isqrt();
if root.square() == *n {
return (Some(root), lp); }
let target = (n.bit_len() as u32).div_ceil(2) + (64 - params.m.leading_zeros());
let fudge = (64 - params.bound.leading_zeros()) + 10;
let lp_bound = match policy {
LargePrime::Off => 0,
LargePrime::Single => params.bound.saturating_mul(128),
};
let margin = match policy {
LargePrime::Off => 16 + fb.len() / 20,
LargePrime::Single => 48 + fb.len() / 8,
};
let want = fb.len() + margin;
let mut rng = Lcg(n.as_limbs().first().copied().unwrap_or(1) | 1);
let mut relations: Vec<Relation> = Vec::new();
let mut logs = alloc::vec![0u8; (2 * params.m + 1) as usize];
let max_polys: u64 = 400_000;
let mut polys = 0u64;
'outer: while relations.len() < want && polys < max_polys {
let Some((a, a_idx)) = choose_a(n, &fb, ¶ms, &mut rng) else {
return (None, lp);
};
let s = a_idx.len();
let mut ac = init_a(&fb, params.m, a, a_idx);
let mut signs = alloc::vec![1i64; s];
let mut b = {
let mut acc = Int::ZERO;
for t in &ac.b_terms {
acc = acc.add(&Int::from(t.clone()));
}
acc
};
let polys_this_a = 1u64 << (s - 1);
for iter in 0..polys_this_a {
if iter > 0 {
let j = iter.trailing_zeros() as usize;
let ds = signs[j];
signs[j] = -ds;
let two_bj = Int::from(ac.b_terms[j].clone()).mul(&Int::from_u64(2));
b = if ds > 0 {
b.sub(&two_bj)
} else {
b.add(&two_bj)
};
for (idx, fp) in fb.iter().enumerate().skip(1) {
if !ac.active[idx] {
continue;
}
let p = fp.p as i128;
let inc = ds as i128 * ac.bainv2[idx * s + j] as i128;
ac.soln1[idx] = ((ac.soln1[idx] as i128 + inc).rem_euclid(p)) as u64;
ac.soln2[idx] = ((ac.soln2[idx] as i128 + inc).rem_euclid(p)) as u64;
}
}
sieve_poly(
n,
&ac,
&b,
&fb,
params.m,
target,
fudge,
lp_bound,
&mut logs,
&mut relations,
&mut lp,
want,
);
polys += 1;
if relations.len() >= want {
break 'outer;
}
}
}
if relations.len() <= fb.len() {
return (None, lp);
}
let deps = find_dependencies(&relations, fb.len());
(factor_from_dependencies(n, &fb, &relations, &deps), lp)
}
#[cfg(test)]
mod tests {
use super::*;
fn prime_at_least(start: u64) -> Nat {
let mut c = start | 1;
loop {
let v = Nat::from_u64(c);
if v.is_prime_bpsw() {
return v;
}
c += 2;
}
}
fn assert_splits(p: &Nat, q: &Nat) {
let composite = p.mul(q);
let f = qs_factor(&composite).expect("QS finds a factor");
assert!(f == *p || f == *q, "factor {f:?} is one of the primes");
let (cof, r) = composite.div_rem(&f).unwrap();
assert!(r.is_zero());
assert!(cof == *p || cof == *q);
}
#[test]
fn splits_balanced_semiprimes() {
assert_splits(
&prime_at_least(3_000_000_019),
&prime_at_least(4_000_000_007),
);
assert_splits(
&prime_at_least(5_000_000_000_021),
&prime_at_least(9_000_000_000_011),
);
}
fn big_prime_at_least(start: &Nat) -> Nat {
let mut c = if start.is_even() {
start.add(&Nat::one())
} else {
start.clone()
};
loop {
if c.is_prime_bpsw() {
return c;
}
c = c.add(&Nat::from_u64(2));
}
}
fn assert_siqs_splits(p: &Nat, q: &Nat) {
let n = p.mul(q);
let f = siqs_factor(&n).expect("SIQS finds a factor");
assert!(f == *p || f == *q, "factor {f:?} is one of the primes");
let (cof, r) = n.div_rem(&f).unwrap();
assert!(r.is_zero());
assert!(cof == *p || cof == *q);
}
#[test]
fn siqs_splits_balanced_semiprimes() {
assert_siqs_splits(
&big_prime_at_least(&Nat::from_u128(3_000_000_000_000_000_000_007u128)),
&big_prime_at_least(&Nat::from_u128(5_000_000_000_000_000_000_003u128)),
);
assert_siqs_splits(
&big_prime_at_least(&Nat::from_u128(200_000_000_000_000_000_000_003u128)),
&big_prime_at_least(&Nat::from_u128(700_000_000_000_000_000_000_001u128)),
);
}
#[test]
fn siqs_handles_perfect_square() {
let p = big_prime_at_least(&Nat::from_u128(40_000_000_000_000_000_019u128));
let n = p.square();
assert_eq!(siqs_factor(&n), Some(p));
}
fn check_relation(n: &Nat, fb: &[FbPrime], rel: &Relation) -> bool {
let lhs = rel.base.square().div_rem(n).unwrap().1;
let mut neg = false;
let mut rhs = Nat::one();
for &(idx, e) in &rel.exps {
if idx == 0 {
neg = e & 1 == 1;
continue;
}
let pe = Nat::from_u64(fb[idx].p).modpow(&Nat::from_u64(e as u64), n);
rhs = rhs.mul(&pe).div_rem(n).unwrap().1;
}
let rhs = if neg {
n.checked_sub(&rhs).unwrap()
} else {
rhs
};
lhs == rhs
}
#[test]
#[ignore = "heavy: release-only relation-invariant check"]
fn combined_partial_relations_are_valid() {
let (n, _p, _q) = balanced_semiprime(38, 3);
let params = siqs_params_for((n.bit_len() * 30103 / 100000 + 1) as usize);
let fb = build_factor_base(&n, params.bound);
let target = (n.bit_len() as u32).div_ceil(2) + (64 - params.m.leading_zeros());
let fudge = (64 - params.bound.leading_zeros()) + 10;
let lp_bound = params.bound.saturating_mul(128);
let m = params.m;
let mut logs = alloc::vec![0u8; (2 * m + 1) as usize];
let mut rng = Lcg(n.as_limbs().first().copied().unwrap_or(1) | 1);
let mut relations: Vec<Relation> = Vec::new();
let mut lp = LpStore::new();
for _ in 0..8 {
let Some((a, a_idx)) = choose_a(&n, &fb, ¶ms, &mut rng) else {
break;
};
let s = a_idx.len();
let mut ac = init_a(&fb, m, a, a_idx);
let mut signs = alloc::vec![1i64; s];
let mut b = {
let mut acc = Int::ZERO;
for t in &ac.b_terms {
acc = acc.add(&Int::from(t.clone()));
}
acc
};
for iter in 0..(1u64 << (s - 1)) {
if iter > 0 {
let j = iter.trailing_zeros() as usize;
let ds = signs[j];
signs[j] = -ds;
let two_bj = Int::from(ac.b_terms[j].clone()).mul(&Int::from_u64(2));
b = if ds > 0 {
b.sub(&two_bj)
} else {
b.add(&two_bj)
};
for (idx, fp) in fb.iter().enumerate().skip(1) {
if !ac.active[idx] {
continue;
}
let p = fp.p as i128;
let inc = ds as i128 * ac.bainv2[idx * s + j] as i128;
ac.soln1[idx] = ((ac.soln1[idx] as i128 + inc).rem_euclid(p)) as u64;
ac.soln2[idx] = ((ac.soln2[idx] as i128 + inc).rem_euclid(p)) as u64;
}
}
sieve_poly(
&n,
&ac,
&b,
&fb,
m,
target,
fudge,
lp_bound,
&mut logs,
&mut relations,
&mut lp,
usize::MAX,
);
}
}
assert!(lp.combined > 0, "expected some combined-partial relations");
for rel in &relations {
assert!(
check_relation(&n, &fb, rel),
"invalid relation base={:?}",
rel.base
);
}
}
fn ten_pow(k: u32) -> Nat {
let ten = Nat::from_u64(10);
let mut r = Nat::one();
for _ in 0..k {
r = r.mul(&ten);
}
r
}
fn balanced_semiprime(d: u32, seed: u64) -> (Nat, Nat, Nat) {
let base = ten_pow(d / 2);
let p = big_prime_at_least(&base.add(&Nat::from_u64(seed.wrapping_mul(2) + 1)));
let q = big_prime_at_least(&base.add(&Nat::from_u64(seed.wrapping_mul(1000) + 12345)));
let n = p.mul(&q);
(n, p, q)
}
#[test]
#[ignore = "heavy: release-only SIQS batch"]
fn siqs_lp_correctness_batch() {
let (mut solved, mut total) = (0u32, 0u32);
for &d in &[30u32, 34, 38, 42, 46] {
for seed in 0..4u64 {
let (n, p, q) = balanced_semiprime(d, seed);
if p == q {
continue;
}
total += 1;
let Some(f) = siqs_factor(&n) else { continue };
solved += 1;
assert!(f == p || f == q, "d={d} seed={seed}: bad factor {f:?}");
let (cof, r) = n.div_rem(&f).unwrap();
assert!(r.is_zero(), "d={d} seed={seed}: not a divisor");
assert!(f.is_prime_bpsw() && cof.is_prime_bpsw());
assert_eq!(f.mul(&cof), n, "d={d} seed={seed}: product mismatch");
}
}
assert!(
solved * 10 >= total * 9,
"solved {solved}/{total}: SIQS+LP solve rate too low"
);
}
#[test]
#[ignore = "benchmark: release-only, prints timings"]
fn siqs_lp_benchmark() {
use std::time::Instant;
std::println!(
"\n{:>4} {:>10} {:>10} {:>8} {:>9} {:>9} {:>7}",
"dig",
"off (ms)",
"lp (ms)",
"speedup",
"combined",
"partials",
"share",
);
let best = |n: &Nat, pol: LargePrime| -> (f64, LpStore) {
let mut ms = f64::MAX;
let mut store = LpStore::new();
for _ in 0..3 {
let t = Instant::now();
let (f, s) = siqs_run(n, pol);
ms = ms.min(t.elapsed().as_secs_f64() * 1e3);
assert!(f.is_some());
store = s;
}
(ms, store)
};
for &d in &[38u32, 42, 44, 46, 48] {
let (n, _p, _q) = balanced_semiprime(d, 1);
let (off_ms, _soff) = best(&n, LargePrime::Off);
let (lp_ms, slp) = best(&n, LargePrime::Single);
let total = slp.direct + slp.combined;
let share = if total > 0 {
100.0 * slp.combined as f64 / total as f64
} else {
0.0
};
std::println!(
"{d:>4} {off_ms:>10.1} {lp_ms:>10.1} {:>7.2}x {:>9} {:>9} {:>6.1}%",
off_ms / lp_ms,
slp.combined,
slp.partials_seen,
share,
);
}
}
}